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> 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> hooks +) implements Module { + public static final String CODE = "fiftyone-devicedetection"; + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> 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> evidenceCaptor = ArgumentCaptor.forClass(Map.class); + verify(flowData).addEvidence(evidenceCaptor.capture()); + final Map evidence = evidenceCaptor.getValue(); + + assertThat(evidence).isNotSameAs(secureHeaders); + assertThat(evidence).containsExactlyEntriesOf(secureHeaders); + } + + @Test + public void populateDeviceInfoShouldPassToFlowDataHeadersMadeFromUaWhenNoSuaPresent() throws Exception { + // given + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("dummy-ua") + .rawHeaders(Collections.singletonMap("ua", "zumba").entrySet()) + .build(); + + // when + target.populateDeviceInfo(null, collectedEvidence); + + // then + final ArgumentCaptor> evidenceCaptor = ArgumentCaptor.forClass(Map.class); + verify(flowData).addEvidence(evidenceCaptor.capture()); + final Map evidence = evidenceCaptor.getValue(); + + assertThat(evidence.size()).isEqualTo(1); + final Map.Entry evidenceFragment = evidence.entrySet().stream().findFirst().get(); + assertThat(evidenceFragment.getKey()).isEqualTo("header.user-agent"); + assertThat(evidenceFragment.getValue()).isEqualTo(collectedEvidence.deviceUA()); + } + + @Test + public void populateDeviceInfoShouldPassToFlowDataMergedHeadersMadeFromUaAndSuaWhenBothPresent() throws Exception { + // given + final Map suaHeaders = Collections.singletonMap("ua", "fake-ua"); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .secureHeaders(suaHeaders) + .deviceUA("dummy-ua") + .rawHeaders(Collections.singletonMap("ua", "zumba").entrySet()) + .build(); + + // when + target.populateDeviceInfo(null, collectedEvidence); + + // then + final ArgumentCaptor> evidenceCaptor = ArgumentCaptor.forClass(Map.class); + verify(flowData).addEvidence(evidenceCaptor.capture()); + final Map evidence = evidenceCaptor.getValue(); + + assertThat(evidence).isNotEqualTo(suaHeaders); + assertThat(evidence).containsAllEntriesOf(suaHeaders); + assertThat(evidence).containsEntry("header.user-agent", collectedEvidence.deviceUA()); + assertThat(evidence.size()).isEqualTo(suaHeaders.size() + 1); + } + + @Test + public void populateDeviceInfoShouldPassToFlowDataRawHeaderWhenNoDeviceInfoPresent() throws Exception { + // given + final List> rawHeaders = List.of( + new AbstractMap.SimpleEntry<>("ua", "zumba"), + new AbstractMap.SimpleEntry<>("sec-ua", "astrolabe") + ); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .rawHeaders(rawHeaders) + .build(); + + // when + target.populateDeviceInfo(null, collectedEvidence); + + // then + final ArgumentCaptor> evidenceCaptor = ArgumentCaptor.forClass(Map.class); + verify(flowData).addEvidence(evidenceCaptor.capture()); + final Map evidence = evidenceCaptor.getValue(); + + final List> evidenceFragments = evidence.entrySet().stream().toList(); + assertThat(evidenceFragments.size()).isEqualTo(rawHeaders.size()); + for (int i = 0, n = rawHeaders.size(); i < n; ++i) { + final Map.Entry rawEntry = rawHeaders.get(i); + final Map.Entry newEntry = evidenceFragments.get(i); + assertThat(newEntry.getKey()).isEqualTo("header." + rawEntry.getKey()); + assertThat(newEntry.getValue()).isEqualTo(rawEntry.getValue()); + } + } + + @Test + public void populateDeviceInfoShouldPassToFlowDataLatestRawHeaderWhenMultiplePresentWithSameKey() throws Exception { + // given + final String theKey = "ua"; + final List> rawHeaders = List.of( + new AbstractMap.SimpleEntry<>(theKey, "zumba"), + new AbstractMap.SimpleEntry<>(theKey, "astrolabe") + ); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .rawHeaders(rawHeaders) + .build(); + + // when + target.populateDeviceInfo(null, collectedEvidence); + + // then + final ArgumentCaptor> evidenceCaptor = ArgumentCaptor.forClass(Map.class); + verify(flowData).addEvidence(evidenceCaptor.capture()); + final Map evidence = evidenceCaptor.getValue(); + + final List> evidenceFragments = evidence.entrySet().stream().toList(); + assertThat(evidenceFragments.size()).isEqualTo(1); + assertThat(evidenceFragments.get(0).getValue()).isEqualTo(rawHeaders.get(1).getValue()); + } + + @Test + public void populateDeviceInfoShouldPassToFlowDataEmptyMapWhenNoEvidenceToPick() throws Exception { + // given + final CollectedEvidence collectedEvidence = CollectedEvidence.builder().build(); + + // when + target.populateDeviceInfo(null, collectedEvidence); + + // then + final ArgumentCaptor> evidenceCaptor = ArgumentCaptor.forClass(Map.class); + verify(flowData).addEvidence(evidenceCaptor.capture()); + final Map evidence = evidenceCaptor.getValue(); + + assertThat(evidence).isNotNull(); + assertThat(evidence).isEmpty(); + } + + @Test + public void populateDeviceInfoShouldEnrichAllPropertiesWhenDeviceIsEmpty() throws Exception { + // given + final Device device = Device.builder().build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(device, collectedEvidence); + + // then + assertThat(result.enrichedFields()).containsExactly( + "devicetype", + "make", + "model", + "os", + "osv", + "h", + "w", + "ppi", + "pxratio", + "ext.fiftyonedegrees_deviceId" + ); + } + + @Test + public void populateDeviceInfoShouldReturnNullWhenDeviceIsFull() throws Exception { + // given and when + buildCompleteDeviceData(); + final Device device = buildCompleteDevice(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(device, collectedEvidence); + + // then + assertThat(result).isNull(); + } + + @Test + public void populateDeviceInfoShouldEnrichDeviceTypeWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .devicetype(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getDevicetype()).isEqualTo(buildCompleteDevice().getDevicetype()); + } + + @Test + public void populateDeviceInfoShouldEnrichMakeWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .make(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getMake()).isEqualTo(buildCompleteDevice().getMake()); + } + + @Test + public void populateDeviceInfoShouldEnrichModelWithHWNameWhenHWModelIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .model(null) + .build(); + final String expectedModel = "NinjaTech8888"; + when(deviceData.getHardwareName()) + .thenReturn(aspectPropertyValueWith(Collections.singletonList(expectedModel))); + when(deviceData.getHardwareModel()).thenThrow(new RuntimeException()); + + // when + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getModel()).isEqualTo(expectedModel); + } + + @Test + public void populateDeviceInfoShouldEnrichModelWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .model(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getModel()).isEqualTo(buildCompleteDevice().getModel()); + } + + @Test + public void populateDeviceInfoShouldEnrichOsWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .os(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getOs()).isEqualTo(buildCompleteDevice().getOs()); + } + + @Test + public void populateDeviceInfoShouldEnrichOsvWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .osv(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getOsv()).isEqualTo(buildCompleteDevice().getOsv()); + } + + @Test + public void populateDeviceInfoShouldEnrichHWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .h(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getH()).isEqualTo(buildCompleteDevice().getH()); + } + + @Test + public void populateDeviceInfoShouldEnrichWWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .w(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getW()).isEqualTo(buildCompleteDevice().getW()); + } + + @Test + public void populateDeviceInfoShouldEnrichPpiWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .ppi(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getPpi()).isEqualTo(buildCompleteDevice().getPpi()); + } + + @Test + public void populateDeviceInfoShouldReturnNullWhenScreenInchesHeightIsZero() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .ppi(null) + .build(); + + // when + buildCompleteDeviceData(); + when(deviceData.getScreenInchesHeight()).thenReturn(aspectPropertyValueWith(0.0)); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result).isNull(); + } + + @Test + public void populateDeviceInfoShouldEnrichPXRatioWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .pxratio(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getPxratio()).isEqualTo(buildCompleteDevice().getPxratio()); + } + + @Test + public void populateDeviceInfoShouldEnrichDeviceIDWhenItIsMissing() throws Exception { + // given + final Device testDevice = buildCompleteDevice().toBuilder() + .ext(null) + .build(); + + // when + buildCompleteDeviceData(); + final CollectedEvidence collectedEvidence = CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build(); + final EnrichmentResult result = target.populateDeviceInfo(testDevice, collectedEvidence); + + // then + assertThat(result.enrichedFields()).hasSize(1); + assertThat(result.enrichedDevice().getExt().getProperty("fiftyonedegrees_deviceId").textValue()) + .isEqualTo("fake-device-id"); + } + + private static Device buildCompleteDevice() { + final Device device = Device.builder() + .devicetype(1) + .make("StarFleet") + .model("communicator") + .os("NeutronAI") + .osv("X-502") + .h(5051) + .w(3001) + .ppi(1010) + .pxratio(BigDecimal.valueOf(1.5)) + .ext(ExtDevice.empty()) + .build(); + device.getExt().addProperty("fiftyonedegrees_deviceId", new TextNode("fake-device-id")); + return device; + } + + private void buildCompleteDeviceData() { + when(deviceData.getDeviceType()).thenReturn(aspectPropertyValueWith("Mobile")); + when(deviceData.getHardwareVendor()).thenReturn(aspectPropertyValueWith("StarFleet")); + when(deviceData.getHardwareModel()).thenReturn(aspectPropertyValueWith("communicator")); + when(deviceData.getPlatformName()).thenReturn(aspectPropertyValueWith("NeutronAI")); + when(deviceData.getPlatformVersion()).thenReturn(aspectPropertyValueWith("X-502")); + when(deviceData.getScreenPixelsHeight()).thenReturn(aspectPropertyValueWith(5051)); + when(deviceData.getScreenPixelsWidth()).thenReturn(aspectPropertyValueWith(3001)); + when(deviceData.getScreenInchesHeight()).thenReturn(aspectPropertyValueWith(5.0)); + when(deviceData.getPixelRatio()).thenReturn(aspectPropertyValueWith(1.5)); + when(deviceData.getDeviceId()).thenReturn(aspectPropertyValueWith("fake-device-id")); + } + + @Test + public void populateDeviceInfoShouldEnrichDeviceTypeWithFourWhenDeviceTypeStringIsPhone() throws Exception { + // given + final String typeString = "Phone"; + + // when + when(deviceData.getDeviceType()).thenReturn(aspectPropertyValueWith(typeString)); + final EnrichmentResult result = target.populateDeviceInfo( + null, + CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build()); + final Integer foundValue = result.enrichedDevice().getDevicetype(); + + // then + assertThat(foundValue).isEqualTo(4); + } + + @Test + public void populateDeviceInfoShouldEnrichDeviceTypeWithSevenWhenDeviceTypeStringIsMediaHub() throws Exception { + // given + final String typeString = "MediaHub"; + + // when + when(deviceData.getDeviceType()).thenReturn(aspectPropertyValueWith(typeString)); + final EnrichmentResult result = target.populateDeviceInfo( + null, + CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build()); + final Integer foundValue = result.enrichedDevice().getDevicetype(); + + // then + assertThat(foundValue).isEqualTo(7); + } + + @Test + public void populateDeviceInfoShouldReturnNullWhenDeviceTypeStringIsUnexpected() throws Exception { + // given + final String typeString = "BattleStar Atlantis"; + + // when + when(deviceData.getDeviceType()).thenReturn(aspectPropertyValueWith(typeString)); + final EnrichmentResult result = target.populateDeviceInfo( + null, + CollectedEvidence.builder() + .deviceUA("fake-UserAgent") + .build()); + + // then + assertThat(result).isNull(); + } + + private static AspectPropertyValue aspectPropertyValueWith(T value) { + return new AspectPropertyValue<>() { + @Override + public boolean hasValue() { + return true; + } + + @Override + public T getValue() throws NoValueException { + return value; + } + + @Override + public void setValue(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public String getNoValueMessage() { + throw new UnsupportedOperationException(); + } + + @Override + public void setNoValueMessage(String s) { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/PipelineBuilderTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/PipelineBuilderTest.java new file mode 100644 index 00000000000..0d149d03c9c --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/PipelineBuilderTest.java @@ -0,0 +1,308 @@ +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 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.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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PipelineBuilderTest { + + private ModuleConfig moduleConfig; + private DataFileUpdate dataFileUpdate; + private PerformanceConfig performanceConfig; + + @Mock + private DeviceDetectionPipelineBuilder builderPrime; + @Mock(strictness = LENIENT) + private DeviceDetectionOnPremisePipelineBuilder builder; + @Mock + private Pipeline pipeline; + + @BeforeEach + public void setUp() throws Exception { + dataFileUpdate = new DataFileUpdate(); + performanceConfig = new PerformanceConfig(); + moduleConfig = new ModuleConfig(); + moduleConfig.setDataFile(new DataFile()); + moduleConfig.getDataFile().setUpdate(dataFileUpdate); + moduleConfig.setPerformance(performanceConfig); + when(builderPrime.useOnPremise(any(), anyBoolean())).thenReturn(builder); + when(builder.build()).thenReturn(pipeline); + } + + @Test + public void buildShouldIgnoreEmptyUrl() throws Exception { + // given + dataFileUpdate.setUrl(""); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder, never()).setPerformanceProfile(any()); + } + + @Test + public void buildShouldAssignURL() throws Exception { + // given + dataFileUpdate.setUrl("http://void/"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setDataUpdateUrl(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues()).containsExactly(dataFileUpdate.getUrl()); + } + + @Test + public void buildShouldIgnoreEmptyLicenseKey() throws Exception { + // given + dataFileUpdate.setLicenseKey(""); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder, never()).setDataUpdateLicenseKey(any()); + } + + @Test + public void buildShouldAssignKey() throws Exception { + // given + dataFileUpdate.setLicenseKey("687-398475-34876-384678-34756-3487"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setDataUpdateLicenseKey(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues()).containsExactly(dataFileUpdate.getLicenseKey()); + } + + @Test + public void buildShouldAssignAuto() throws Exception { + // given + dataFileUpdate.setAuto(true); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Boolean.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setAutoUpdate(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues()).containsExactly(dataFileUpdate.getAuto()); + } + + @Test + public void buildShouldAssignOnStartup() throws Exception { + // given + dataFileUpdate.setOnStartup(true); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Boolean.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setDataUpdateOnStartup(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues()).containsExactly(dataFileUpdate.getOnStartup()); + } + + @Test + public void buildShouldAssignWatchFileSystem() throws Exception { + // given + dataFileUpdate.setWatchFileSystem(true); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Boolean.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setDataFileSystemWatcher(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues()).containsExactly(dataFileUpdate.getWatchFileSystem()); + } + + @Test + public void buildShouldAssignPollingInterval() throws Exception { + // given + dataFileUpdate.setPollingInterval(643); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setUpdatePollingInterval(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues()).containsExactly(dataFileUpdate.getPollingInterval()); + } + + @Test + public void buildShouldThrowWhenProfileIsUnknown() { + // given + performanceConfig.setProfile("ghost"); + + try { + // when + assertThatThrownBy(() -> new PipelineBuilder(moduleConfig).build(builderPrime)) + .isInstanceOf(IllegalArgumentException.class); + } finally { + // then + verify(builder, never()).setPerformanceProfile(any()); + } + } + + @Test + public void buildShouldIgnoreEmptyProfile() throws Exception { + // given + performanceConfig.setProfile(""); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder, never()).setPerformanceProfile(any()); + } + + @Test + public void buildShouldAssignMaxPerformance() throws Exception { + // given + performanceConfig.setProfile("mAxperFORMance"); + + final ArgumentCaptor profilesArgumentCaptor + = ArgumentCaptor.forClass(Constants.PerformanceProfiles.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setPerformanceProfile(profilesArgumentCaptor.capture()); + assertThat(profilesArgumentCaptor.getAllValues()).containsExactly(Constants.PerformanceProfiles.MaxPerformance); + } + + @Test + public void buildShouldAssignConcurrency() throws Exception { + // given + performanceConfig.setConcurrency(398476); + + final ArgumentCaptor concurrenciesArgumentCaptor = ArgumentCaptor.forClass(Integer.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setConcurrency(concurrenciesArgumentCaptor.capture()); + assertThat(concurrenciesArgumentCaptor.getAllValues()).containsExactly(performanceConfig.getConcurrency()); + } + + @Test + public void buildShouldAssignDifference() throws Exception { + // given + performanceConfig.setDifference(498756); + + final ArgumentCaptor profilesArgumentCaptor = ArgumentCaptor.forClass(Integer.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setDifference(profilesArgumentCaptor.capture()); + assertThat(profilesArgumentCaptor.getAllValues()).containsExactly(performanceConfig.getDifference()); + } + + @Test + public void buildShouldAssignAllowUnmatched() throws Exception { + // given + performanceConfig.setAllowUnmatched(true); + + final ArgumentCaptor allowUnmatchedArgumentCaptor = ArgumentCaptor.forClass(Boolean.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setAllowUnmatched(allowUnmatchedArgumentCaptor.capture()); + assertThat(allowUnmatchedArgumentCaptor.getAllValues()).containsExactly(performanceConfig.getAllowUnmatched()); + } + + @Test + public void buildShouldAssignDrift() throws Exception { + // given + performanceConfig.setDrift(1348); + + final ArgumentCaptor driftsArgumentCaptor = ArgumentCaptor.forClass(Integer.class); + + // when + new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + verify(builder).setDrift(driftsArgumentCaptor.capture()); + assertThat(driftsArgumentCaptor.getAllValues()).containsExactly(performanceConfig.getDrift()); + } + + @Test + public void buildShouldReturnNonNull() throws Exception { + // given + moduleConfig.getDataFile().setPath("dummy.hash"); + + // when + final Pipeline returnedPipeline = new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + assertThat(returnedPipeline).isEqualTo(pipeline); + } + + @Test + public void buildShouldReturnNonNullWithCopy() throws Exception { + // given + moduleConfig.getDataFile().setPath("dummy.hash"); + moduleConfig.getDataFile().setMakeTempCopy(true); + + // when + final Pipeline returnedPipeline = new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + assertThat(returnedPipeline).isEqualTo(pipeline); + } + + @Test + public void buildShouldNotThrowWhenMinimal() throws Exception { + // given + moduleConfig.getDataFile().setPath("dummy.hash"); + moduleConfig.getDataFile().setUpdate(null); + moduleConfig.setPerformance(null); + + // when + final Pipeline returnedPipeline = new PipelineBuilder(moduleConfig).build(builderPrime); + + // then + assertThat(returnedPipeline).isEqualTo(pipeline); + } +} diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetrieverTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetrieverTest.java new file mode 100644 index 00000000000..e80cc69044b --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetrieverTest.java @@ -0,0 +1,130 @@ +package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core; + +import com.iab.openrtb.request.BrandVersion; +import com.iab.openrtb.request.UserAgent; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SecureHeadersRetrieverTest { + + @Test + public void callShouldAddEmptyMapOfSecureHeadersWhenUserAgentIsEmpty() { + // given + final UserAgent userAgent = UserAgent.builder().build(); + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence).isEmpty(); + } + + @Test + public void callShouldAddBrowsersToSecureHeaders() { + // given + final UserAgent userAgent = UserAgent.builder() + .browsers(List.of( + new BrandVersion("Nickel", List.of("6", "3", "1", "a"), null), + new BrandVersion(null, List.of("7", "52"), null), // should be skipped + new BrandVersion("FrostCat", List.of("9", "2", "5", "8"), null) + )) + .build(); + final String expectedBrowsers = "\"Nickel\";v=\"6.3.1.a\", \"FrostCat\";v=\"9.2.5.8\""; + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence.size()).isEqualTo(2); + assertThat(evidence.get("header.Sec-CH-UA")).isEqualTo(expectedBrowsers); + assertThat(evidence.get("header.Sec-CH-UA-Full-Version-List")).isEqualTo(expectedBrowsers); + } + + @Test + public void callShouldAddPlatformToSecureHeaders() { + final UserAgent userAgent = UserAgent.builder() + .platform(new BrandVersion("Cyborg", List.of("19", "5"), null)) + .build(); + final String expectedPlatformName = "\"Cyborg\""; + final String expectedPlatformVersion = "\"19.5\""; + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence.size()).isEqualTo(2); + assertThat(evidence.get("header.Sec-CH-UA-Platform")).isEqualTo(expectedPlatformName); + assertThat(evidence.get("header.Sec-CH-UA-Platform-Version")).isEqualTo(expectedPlatformVersion); + } + + @Test + public void callShouldAddIsMobileToSecureHeaders() { + final UserAgent userAgent = UserAgent.builder() + .mobile(5) + .build(); + final String expectedIsMobile = "?5"; + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence.size()).isEqualTo(1); + assertThat(evidence.get("header.Sec-CH-UA-Mobile")).isEqualTo(expectedIsMobile); + } + + @Test + public void callShouldAddArchitectureToSecureHeaders() { + final UserAgent userAgent = UserAgent.builder() + .architecture("LEG") + .build(); + final String expectedArchitecture = "\"LEG\""; + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence.size()).isEqualTo(1); + assertThat(evidence.get("header.Sec-CH-UA-Arch")).isEqualTo(expectedArchitecture); + } + + @Test + public void callShouldAddBitnessToSecureHeaders() { + final UserAgent userAgent = UserAgent.builder() + .bitness("doubtful") + .build(); + final String expectedBitness = "\"doubtful\""; + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence.size()).isEqualTo(1); + assertThat(evidence.get("header.Sec-CH-UA-Bitness")).isEqualTo(expectedBitness); + } + + @Test + public void callShouldAddModelToSecureHeaders() { + final UserAgent userAgent = UserAgent.builder() + .model("reflectivity") + .build(); + final String expectedModel = "\"reflectivity\""; + + // when + final Map evidence = SecureHeadersRetriever.retrieveFrom(userAgent); + + // then + assertThat(evidence).isNotNull(); + assertThat(evidence.size()).isEqualTo(1); + assertThat(evidence.get("header.Sec-CH-UA-Model")).isEqualTo(expectedModel); + } +} diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHookTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHookTest.java new file mode 100644 index 00000000000..5acd50c034e --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHookTest.java @@ -0,0 +1,71 @@ +package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks; + +import io.vertx.core.Future; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence; +import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.FiftyOneDeviceDetectionModule; +import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FiftyOneDeviceDetectionEntrypointHookTest { + + private final EntrypointHook target = new FiftyOneDeviceDetectionEntrypointHook(); + + @Test + public void codeShouldStartWithModuleCode() { + // when and then + assertThat(target.code()).startsWith(FiftyOneDeviceDetectionModule.CODE); + } + + @Test + public void callShouldReturnPatchedModule() { + // given + final EntrypointPayload entrypointPayload = EntrypointPayloadImpl.of( + null, + CaseInsensitiveMultiMap.builder().build(), + null + ); + + // when + final Future> result = target.call(entrypointPayload, null); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result().moduleContext()).isNotNull(); + } + + @Test + public void callShouldAddRawRequestHeadersToModuleEvidence() { + // given + final String key = "ua"; + final String value = "AI-scape Imitator"; + final EntrypointPayload entrypointPayload = EntrypointPayloadImpl.of( + null, + CaseInsensitiveMultiMap.builder() + .add(key, value) + .build(), + null + ); + + // when + final Future> result = target.call(entrypointPayload, null); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result().moduleContext()).isInstanceOf(ModuleContext.class); + final CollectedEvidence evidence = ((ModuleContext) result.result().moduleContext()).collectedEvidence(); + assertThat(evidence).isNotNull(); + assertThat(evidence.rawHeaders()).hasSize(1); + final Map.Entry firstHeader = evidence.rawHeaders().stream().findFirst().get(); + assertThat(firstHeader.getKey()).isEqualTo(key); + assertThat(firstHeader.getValue()).isEqualTo(value); + } +} diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java new file mode 100644 index 00000000000..cfd079299a0 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java @@ -0,0 +1,889 @@ +package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks; + +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.UserAgent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; +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.FiftyOneDeviceDetectionModule; +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.model.ModuleContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.settings.model.Account; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FiftyOneDeviceDetectionRawAuctionRequestHookTest { + + @Mock + private DeviceEnricher deviceEnricher; + private AccountFilter accountFilter; + + private RawAuctionRequestHook target; + + @BeforeEach + public void setUp() { + accountFilter = new AccountFilter(); + target = new FiftyOneDeviceDetectionRawAuctionRequestHook(accountFilter, deviceEnricher); + } + + @Test + public void callShouldMakeNewContextWhenNullIsPassedIn() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(null) + .build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final ModuleContext newContext = (ModuleContext) target.call(auctionRequestPayload, invocationContext) + .result() + .moduleContext(); + + // then + assertThat(newContext).isNotNull(); + assertThat(newContext.collectedEvidence()).isNotNull(); + } + + @Test + public void callShouldMakeNewEvidenceWhenNoneWasPresent() { + // given + final ModuleContext moduleContext = ModuleContext.builder().build(); + final BidRequest bidRequest = BidRequest.builder() + .device(null) + .build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + moduleContext + ); + + // when + final ModuleContext newContext = (ModuleContext) target.call(auctionRequestPayload, invocationContext) + .result() + .moduleContext(); + + // then + assertThat(newContext).isNotNull(); + assertThat(newContext.collectedEvidence()).isNotNull(); + } + + @Test + public void callShouldMergeEvidences() { + // given + final String ua = "mad-hatter"; + final HashMap sua = new HashMap<>(); + final ModuleContext existingContext = ModuleContext.builder() + .collectedEvidence(CollectedEvidence.builder() + .secureHeaders(sua) + .build()) + .build(); + final Device device = Device.builder().ua(ua).build(); + final BidRequest bidRequest = BidRequest.builder() + .device(device) + .build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + existingContext + ); + + // when + final ModuleContext newContext = (ModuleContext) target.call(auctionRequestPayload, invocationContext) + .result() + .moduleContext(); + + // then + assertThat(newContext).isNotNull(); + final CollectedEvidence newEvidence = newContext.collectedEvidence(); + assertThat(newEvidence).isNotNull(); + assertThat(newEvidence.deviceUA()).isEqualTo(ua); + assertThat(newEvidence.secureHeaders()).isEqualTo(sua); + } + + @Test + public void callShouldNotFailWhenNoDevice() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final CollectedEvidence evidence = ((ModuleContext) target.call(payload, auctionInvocationContext) + .result() + .moduleContext()) + .collectedEvidence(); + + // then + assertThat(evidence).isNotNull(); + } + + @Test + public void callShouldAddUAToModuleContextEvidence() { + // given + final String testUA = "MindScape Crawler"; + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua(testUA).build()) + .build(); + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final CollectedEvidence evidence = ((ModuleContext) target.call(payload, auctionInvocationContext) + .result() + .moduleContext()) + .collectedEvidence(); + + // then + assertThat(evidence.deviceUA()).isEqualTo(testUA); + } + + @Test + public void callShouldAddSUAToModuleContextEvidence() { + // given + final UserAgent testSUA = UserAgent.builder().build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().sua(testSUA).build()) + .build(); + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext auctionInvocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final CollectedEvidence evidence = ((ModuleContext) target.call(payload, auctionInvocationContext) + .result() + .moduleContext()) + .collectedEvidence(); + + // then + assertThat(evidence.secureHeaders()).isEmpty(); + } + + @Test + public void payloadUpdateShouldReturnNullWhenRequestIsNull() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(null); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + ModuleContext.builder() + .collectedEvidence(null) + .build() + ); + + // when + final BidRequest newBidRequest = target.call(auctionRequestPayload, invocationContext) + .result() + .payloadUpdate() + .apply(auctionRequestPayload) + .bidRequest(); + + // then + assertThat(newBidRequest).isNull(); + } + + @Test + public void payloadUpdateShouldReturnOldRequestWhenPopulateDeviceInfoThrows() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final CollectedEvidence savedEvidence = CollectedEvidence.builder().build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + ModuleContext.builder() + .collectedEvidence(savedEvidence) + .build() + ); + final Exception e = new RuntimeException(); + when(deviceEnricher.populateDeviceInfo(any(), any())).thenThrow(e); + + // when + final BidRequest newBidRequest = target.call(auctionRequestPayload, invocationContext) + .result() + .payloadUpdate() + .apply(auctionRequestPayload) + .bidRequest(); + + // then + assertThat(newBidRequest).isEqualTo(bidRequest); + verify(deviceEnricher, times(1)).populateDeviceInfo(any(), any()); + } + + @Test + public void payloadUpdateShouldReturnOldRequestWhenMergedDeviceIsNull() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final CollectedEvidence savedEvidence = CollectedEvidence.builder().build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + ModuleContext.builder() + .collectedEvidence(savedEvidence) + .build() + ); + when(deviceEnricher.populateDeviceInfo(any(), any())) + .thenReturn(EnrichmentResult.builder().build()); + + // when + final BidRequest newBidRequest = target.call(auctionRequestPayload, invocationContext) + .result() + .payloadUpdate() + .apply(auctionRequestPayload) + .bidRequest(); + + // then + assertThat(newBidRequest).isEqualTo(bidRequest); + verify(deviceEnricher, times(1)).populateDeviceInfo(any(), any()); + } + + @Test + public void payloadUpdateShouldPassMergedEvidenceToDeviceRefiner() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final String fakeUA = "crystal-ball-navigator"; + final CollectedEvidence savedEvidence = CollectedEvidence.builder() + .rawHeaders(Collections.emptySet()) + .deviceUA(fakeUA) + .build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + ModuleContext.builder() + .collectedEvidence(savedEvidence) + .build() + ); + when(deviceEnricher.populateDeviceInfo(any(), any())) + .thenReturn(EnrichmentResult.builder().build()); + + // when + final BidRequest newBidRequest = target.call(auctionRequestPayload, invocationContext) + .result() + .payloadUpdate() + .apply(auctionRequestPayload) + .bidRequest(); + + // then + assertThat(newBidRequest).isEqualTo(bidRequest); + verify(deviceEnricher, times(1)).populateDeviceInfo(any(), any()); + + final ArgumentCaptor evidenceCaptor = ArgumentCaptor.forClass(CollectedEvidence.class); + verify(deviceEnricher).populateDeviceInfo(any(), evidenceCaptor.capture()); + final List allEvidences = evidenceCaptor.getAllValues(); + assertThat(allEvidences).hasSize(1); + assertThat(allEvidences.getFirst().deviceUA()).isEqualTo(fakeUA); + } + + @Test + public void payloadUpdateShouldInjectReturnedDevice() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final CollectedEvidence savedEvidence = CollectedEvidence.builder().build(); + final Device mergedDevice = Device.builder().build(); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final AuctionInvocationContext invocationContext = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + ModuleContext.builder() + .collectedEvidence(savedEvidence) + .build() + ); + when(deviceEnricher.populateDeviceInfo(any(), any())) + .thenReturn(EnrichmentResult + .builder() + .enrichedDevice(mergedDevice) + .build()); + + // when + final BidRequest newBidRequest = target.call(auctionRequestPayload, invocationContext) + .result() + .payloadUpdate() + .apply(auctionRequestPayload) + .bidRequest(); + + // then + assertThat(newBidRequest.getDevice()).isEqualTo(mergedDevice); + verify(deviceEnricher, times(1)).populateDeviceInfo(any(), any()); + } + + @Test + public void codeShouldStartWithModuleCode() { + // when and then + assertThat(target.code()).startsWith(FiftyOneDeviceDetectionModule.CODE); + } + + @Test + public void callShouldReturnUpdateActionWhenFilterIsNull() { + // given + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAuctionContext() { + // given + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistEmptyAndNoAuctionContext() { + // given + accountFilter.setAllowList(Collections.emptyList()); + + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAuctionContext() { + // given + accountFilter.setAllowList(Collections.singletonList("42")); + + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + null, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccount() { + // given + final AuctionContext auctionContext = AuctionContext.builder().build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnNoUpdateActionWhenNoWhitelistAndNoAccountButDeviceIdIsSet() { + // given + final AuctionContext auctionContext = AuctionContext.builder().build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + final ExtDevice ext = ExtDevice.empty(); + final Device device = Device.builder().ext(ext).build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(bidRequest); + ext.addProperty("fiftyonedegrees_deviceId", new TextNode("0-0-0-0")); + + // when + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistEmptyAndNoAccount() { + // given + accountFilter.setAllowList(Collections.emptyList()); + + final AuctionContext auctionContext = AuctionContext.builder().build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAccount() { + // given + accountFilter.setAllowList(Collections.singletonList("42")); + + final AuctionContext auctionContext = AuctionContext.builder().build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccountID() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistEmptyAndNoAccountID() { + // given + accountFilter.setAllowList(Collections.emptyList()); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAccountID() { + // given + accountFilter.setAllowList(Collections.singletonList("42")); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnUpdateActionWhenNoWhitelistAndEmptyAccountID() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistEmptyAndEmptyAccountID() { + // given + accountFilter.setAllowList(Collections.emptyList()); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndEmptyAccountID() { + // given + accountFilter.setAllowList(Collections.singletonList("42")); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnUpdateActionWhenNoWhitelistAndAllowedAccountID() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("42") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistEmptyAndAllowedAccountID() { + // given + accountFilter.setAllowList(Collections.emptyList()); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("42") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistFilledAndAllowedAccountID() { + // given + accountFilter.setAllowList(Collections.singletonList("42")); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("42") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenNoWhitelistAndNotAllowedAccountID() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("29") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnUpdateActionWhenWhitelistEmptyAndNotAllowedAccountID() { + // given + accountFilter.setAllowList(Collections.emptyList()); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("29") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNotAllowedAccountID() { + // given + accountFilter.setAllowList(Collections.singletonList("42")); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .id("29") + .build()) + .build(); + final AuctionInvocationContext context = AuctionInvocationContextImpl.of( + null, + auctionContext, + false, + null, + null + ); + + // when + final AuctionRequestPayload payload = AuctionRequestPayloadImpl.of(BidRequest.builder().build()); + final InvocationAction invocationAction = target.call(payload, context) + .result() + .action(); + + // then + assertThat(invocationAction).isEqualTo(InvocationAction.no_action); + } +} diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 27bdb434d58..90fe75bac96 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 2.13.0-SNAPSHOT + 3.15.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java index 599be6e981c..a7ee0425135 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java @@ -18,6 +18,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.spring.config.bidder.model.MediaType; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -51,7 +52,11 @@ public class AccountConfigReader { private static final String ALLOWED_APP_FOR_DEALS_FIELD = "allowed-app-for-deals"; private static final String BLOCKED_BANNER_TYPE_FIELD = "blocked-banner-type"; private static final String BLOCKED_BANNER_ATTR_FIELD = "blocked-banner-attr"; + private static final String BLOCKED_VIDEO_ATTR_FIELD = "blocked-video-attr"; + private static final String BLOCKED_AUDIO_ATTR_FIELD = "blocked-audio-attr"; private static final String ALLOWED_BANNER_ATTR_FOR_DEALS = "allowed-banner-attr-for-deals"; + private static final String ALLOWED_VIDEO_ATTR_FOR_DEALS = "allowed-video-attr-for-deals"; + private static final String ALLOWED_AUDIO_ATTR_FOR_DEALS = "allowed-audio-attr-for-deals"; private static final String ACTION_OVERRIDES_FIELD = "action-overrides"; private static final String OVERRIDE_FIELD = "override"; private static final String CONDITIONS_FIELD = "conditions"; @@ -100,8 +105,14 @@ public Result blockedAttributesFor(BidRequest bidRequest) { blockedAttribute(BAPP_FIELD, String.class, BLOCKED_APP_FIELD, requestMediaTypes); final Result>> btype = blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, bidRequest); - final Result>> battr = + final Result>> bannerBattr = blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest); + final Result>> videoBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, bidRequest); + final Result>> audioBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, bidRequest); + final Result>>> battr = + mergeBlockedAttributes(bannerBattr, videoBattr, audioBattr); return Result.of( toBlockedAttributes(badv, bcat, cattaxComplement, bapp, btype, battr), @@ -133,22 +144,39 @@ public Result responseBlockingConfigFor(BidderBid bidder ALLOWED_APP_FOR_DEALS_FIELD, bidMediaTypes, dealid); - final Result> battr = blockingConfigForAttribute( + final Result> bannerBattr = blockingConfigForAttribute( BATTR_FIELD, Integer.class, ALLOWED_BANNER_ATTR_FOR_DEALS, bidMediaTypes, dealid); + final Result> videoBattr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_VIDEO_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + final Result> audioBattr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_AUDIO_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + final Map> battr = new HashMap<>(); + battr.put(MediaType.BANNER, bannerBattr.getValue()); + battr.put(MediaType.VIDEO, videoBattr.getValue()); + battr.put(MediaType.AUDIO, audioBattr.getValue()); final ResponseBlockingConfig response = ResponseBlockingConfig.builder() .badv(badv.getValue()) .bcat(bcat.getValue()) .cattax(cattax.getValue()) .bapp(bapp.getValue()) - .battr(battr.getValue()) + .battr(battr) .build(); - final List warnings = MergeUtils.mergeMessages(badv, bcat, cattax, bapp, battr); + final List warnings = MergeUtils.mergeMessages( + badv, bcat, cattax, bapp, bannerBattr, videoBattr, audioBattr); return Result.of(response, warnings); } @@ -218,6 +246,28 @@ private Result>> blockedAttributesForImps(String attribu MergeUtils.mergeMessages(results)); } + private static Result>>> mergeBlockedAttributes( + Result>> bannerBattr, + Result>> videoBattr, + Result>> audioBattr) { + + final Map>> battr = new HashMap<>(); + + if (bannerBattr.hasValue()) { + battr.put(MediaType.BANNER, bannerBattr.getValue()); + } + if (videoBattr.hasValue()) { + battr.put(MediaType.VIDEO, videoBattr.getValue()); + } + if (audioBattr.hasValue()) { + battr.put(MediaType.AUDIO, audioBattr.getValue()); + } + + return Result.of( + !battr.isEmpty() ? battr : null, + MergeUtils.mergeMessages(bannerBattr, videoBattr, audioBattr)); + } + private Result> blockingConfigForAttribute(String attribute, Class attributeType, String blockUnknownField, @@ -360,8 +410,8 @@ private Result toResult(List specificBidderResults, Set actualMediaTypes) { final JsonNode value = ObjectUtils.firstNonNull( - specificBidderResults.size() > 0 ? specificBidderResults.get(0) : null, - catchAllBidderResults.size() > 0 ? catchAllBidderResults.get(0) : null); + !specificBidderResults.isEmpty() ? specificBidderResults.getFirst() : null, + !catchAllBidderResults.isEmpty() ? catchAllBidderResults.getFirst() : null); final List warnings = debugEnabled && specificBidderResults.size() + catchAllBidderResults.size() > 1 ? Collections.singletonList( "More than one conditions matches request. Bidder: %s, request media types: %s" @@ -376,7 +426,7 @@ private static BlockedAttributes toBlockedAttributes(Result> badv, Result cattaxComplement, Result> bapp, Result>> btype, - Result>> battr) { + Result>>> battr) { return badv.hasValue() || bcat.hasValue() diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java index 28bdccdd136..2c9b40b6079 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java @@ -5,6 +5,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; @@ -16,6 +18,8 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.ArrayList; import java.util.Collections; @@ -45,6 +49,7 @@ public class BidsBlocker { private final OrtbVersion ortbVersion; private final ObjectNode accountConfig; private final BlockedAttributes blockedAttributes; + private final BidRejectionTracker bidRejectionTracker; private final boolean debugEnabled; private BidsBlocker(List bids, @@ -52,6 +57,7 @@ private BidsBlocker(List bids, OrtbVersion ortbVersion, ObjectNode accountConfig, BlockedAttributes blockedAttributes, + BidRejectionTracker bidRejectionTracker, boolean debugEnabled) { this.bids = bids; @@ -59,6 +65,7 @@ private BidsBlocker(List bids, this.ortbVersion = ortbVersion; this.accountConfig = accountConfig; this.blockedAttributes = blockedAttributes; + this.bidRejectionTracker = bidRejectionTracker; this.debugEnabled = debugEnabled; } @@ -67,6 +74,7 @@ public static BidsBlocker create(List bids, OrtbVersion ortbVersion, ObjectNode accountConfig, BlockedAttributes blockedAttributes, + BidRejectionTracker bidRejectionTracker, boolean debugEnabled) { return new BidsBlocker( @@ -75,6 +83,7 @@ public static BidsBlocker create(List bids, Objects.requireNonNull(ortbVersion), accountConfig, blockedAttributes, + bidRejectionTracker, debugEnabled); } @@ -84,7 +93,6 @@ public ExecutionResult block() { try { final List> blockedBidResults = bids.stream() - .sequential() .map(bid -> isBlocked(bid, accountConfigReader)) .toList(); @@ -96,6 +104,10 @@ public ExecutionResult block() { final BlockedBids blockedBids = !blockedBidIndexes.isEmpty() ? BlockedBids.of(blockedBidIndexes) : null; final List warnings = MergeUtils.mergeMessages(blockedBidResults); + if (blockedBids != null) { + rejectBlockedBids(blockedBidResults); + } + return ExecutionResult.builder() .value(blockedBids) .debugMessages(blockedBids != null ? debugMessages(blockedBidIndexes, blockedBidResults) : null) @@ -159,11 +171,30 @@ private AttributeCheckResult checkBapp(BidderBid bidderBid, ResponseBloc } private AttributeCheckResult checkBattr(BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { - + final MediaType mediaType = mapBidTypeToMediaType(bidderBid.getType()); return checkAttribute( bidderBid.getBid().getAttr(), - blockingConfig.getBattr(), - blockedAttributeValues(BlockedAttributes::getBattr, bidderBid.getBid().getImpid())); + blockingConfig.getBattr().get(mediaType), + blockedAttributeValues( + blockedAttributes -> extractBattrForMediaType(blockedAttributes, mediaType), + bidderBid.getBid().getImpid())); + } + + private static MediaType mapBidTypeToMediaType(BidType bidType) { + return switch (bidType) { + case banner -> MediaType.BANNER; + case video -> MediaType.VIDEO; + case audio -> MediaType.AUDIO; + case xNative -> MediaType.NATIVE; + case null -> null; + }; + } + + private static Map> extractBattrForMediaType(BlockedAttributes blockedAttributes, + MediaType mediaType) { + + final Map>> battr = blockedAttributes.getBattr(); + return battr != null ? battr.get(mediaType) : null; } private AttributeCheckResult checkAttribute(List attribute, @@ -256,6 +287,30 @@ private String debugEntryFor(int index, BlockingResult blockingResult) { blockingResult.getFailedChecks()); } + private void rejectBlockedBids(List> blockedBidResults) { + blockedBidResults.stream() + .map(Result::getValue) + .filter(BlockingResult::isBlocked) + .forEach(this::rejectBlockedBid); + } + + private void rejectBlockedBid(BlockingResult blockingResult) { + if (blockingResult.getBattrCheckResult().isFailed() + || blockingResult.getBappCheckResult().isFailed() + || blockingResult.getBcatCheckResult().isFailed()) { + + bidRejectionTracker.reject( + blockingResult.getImpId(), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + } + + if (blockingResult.getBadvCheckResult().isFailed()) { + bidRejectionTracker.reject( + blockingResult.getImpId(), + BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + } + } + private List toAnalyticsResults(List> blockedBidResults) { return blockedBidResults.stream() .map(Result::getValue) diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java index f83d8554a2c..ac963b94857 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java @@ -1,15 +1,19 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; public class RequestUpdater { @@ -40,39 +44,93 @@ public BidRequest update(BidRequest bidRequest) { private List updateImps(List imps) { final Map> blockedBannerType = blockedAttributes.getBtype(); - final Map> blockedBannerAttr = blockedAttributes.getBattr(); + final Map>> blockedAttr = blockedAttributes.getBattr(); - if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedBannerAttr)) { + if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedAttr)) { return imps; } return imps.stream() - .map(imp -> updateImp(imp, blockedBannerType, blockedBannerAttr)) + .map(imp -> updateImp(imp, blockedBannerType, blockedAttr)) .toList(); } private Imp updateImp(Imp imp, Map> blockedBannerType, - Map> blockedBannerAttr) { + Map>> blockedAttr) { final String impId = imp.getId(); final List btypeForImp = blockedBannerType != null ? blockedBannerType.get(impId) : null; - final List battrForImp = blockedBannerAttr != null ? blockedBannerAttr.get(impId) : null; + final List bannerBattrForImp = extractBattr(blockedAttr, MediaType.BANNER, impId); + final List videoBattrForImp = extractBattr(blockedAttr, MediaType.VIDEO, impId); + final List audioBattrForImp = extractBattr(blockedAttr, MediaType.AUDIO, impId); + + if (CollectionUtils.isEmpty(btypeForImp) + && CollectionUtils.isEmpty(bannerBattrForImp) + && CollectionUtils.isEmpty(videoBattrForImp) + && CollectionUtils.isEmpty(audioBattrForImp)) { - if (CollectionUtils.isEmpty(btypeForImp) && CollectionUtils.isEmpty(battrForImp)) { return imp; } final Banner banner = imp.getBanner(); - final List existingBtype = banner != null ? banner.getBtype() : null; - final List existingBattr = banner != null ? banner.getBattr() : null; - final Banner.BannerBuilder bannerBuilder = banner != null ? banner.toBuilder() : Banner.builder(); + final Video video = imp.getVideo(); + final Audio audio = imp.getAudio(); return imp.toBuilder() - .banner(bannerBuilder - .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btypeForImp) - .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battrForImp) - .build()) + .banner(CollectionUtils.isNotEmpty(btypeForImp) || CollectionUtils.isNotEmpty(bannerBattrForImp) + ? updateBanner(banner, btypeForImp, bannerBattrForImp) + : banner) + .video(CollectionUtils.isNotEmpty(videoBattrForImp) + ? updateVideo(imp.getVideo(), videoBattrForImp) + : video) + .audio(CollectionUtils.isNotEmpty(audioBattrForImp) + ? updateAudio(imp.getAudio(), audioBattrForImp) + : audio) .build(); } + + private static List extractBattr(Map>> blockedAttr, + MediaType mediaType, + String impId) { + + final Map> impIdToBattr = blockedAttr != null ? blockedAttr.get(mediaType) : null; + return impIdToBattr != null ? impIdToBattr.get(impId) : null; + } + + private static Banner updateBanner(Banner banner, List btype, List battr) { + final List existingBtype = banner != null ? banner.getBtype() : null; + final List existingBattr = banner != null ? banner.getBattr() : null; + + return CollectionUtils.isEmpty(existingBtype) || CollectionUtils.isEmpty(existingBattr) + ? Optional.ofNullable(banner) + .map(Banner::toBuilder) + .orElseGet(Banner::builder) + .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btype) + .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battr) + .build() + : banner; + } + + private static Video updateVideo(Video video, List battr) { + final List existingBattr = video != null ? video.getBattr() : null; + return CollectionUtils.isEmpty(existingBattr) + ? Optional.ofNullable(video) + .map(Video::toBuilder) + .orElseGet(Video::builder) + .battr(battr) + .build() + : video; + } + + private static Audio updateAudio(Audio audio, List battr) { + final List existingBattr = audio != null ? audio.getBattr() : null; + return CollectionUtils.isEmpty(existingBattr) + ? Optional.ofNullable(audio) + .map(Audio::toBuilder) + .orElseGet(Audio::builder) + .battr(battr) + .build() + : audio; + } } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java index d3d3049b57c..aad04ba8db6 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; import java.util.Map; @@ -20,5 +21,5 @@ public class BlockedAttributes { Map> btype; - Map> battr; + Map>> battr; } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java index c2108eb8a8f..8c34561079e 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java @@ -2,6 +2,9 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.spring.config.bidder.model.MediaType; + +import java.util.Map; @Builder @Value @@ -15,5 +18,5 @@ public class ResponseBlockingConfig { BidAttributeBlockingConfig bapp; - BidAttributeBlockingConfig battr; + Map> battr; } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java index 0bd01505596..ff9e6ab6c3c 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java @@ -42,7 +42,8 @@ public Future> call(BidderRequestPayload final BidRequest bidRequest = bidderRequestPayload.bidRequest(); final ModuleContext moduleContext = moduleContext(invocationContext) - .with(bidder, bidderSupportedOrtbVersion(bidder, aliases(bidRequest))); + .with(bidder, bidderSupportedOrtbVersion( + bidder, aliases(invocationContext.auctionContext().getBidRequest()))); final ExecutionResult blockedAttributesResult = BlockedAttributesResolver .create( diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java index 7ba82dd5b09..329e99d3bbc 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java @@ -59,6 +59,7 @@ public Future> call(BidderResponsePayloa ObjectUtils.defaultIfNull(moduleContext.ortbVersionOf(bidder), OrtbVersion.ORTB_2_5), invocationContext.accountConfig(), moduleContext.blockedAttributesFor(bidder), + invocationContext.auctionContext().getBidRejectionTrackers().get(bidder), invocationContext.debugEnabled()) .block(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java index 52035b7a2b4..f6568663807 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java @@ -12,7 +12,7 @@ import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AllowedForDealsOverride; @@ -30,6 +30,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.HashMap; import java.util.HashSet; @@ -734,7 +735,7 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri } @Test - public void blockedAttributesForShouldReturnResultWithAllAttributes() { + public void blockedAttributesForShouldReturnResultWithAllAttributesForBanner() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -766,7 +767,7 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { Conditions.of(singletonList("bidder1"), null), singletonList(3))))) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .blocked(asList(1, 2)) .actionOverrides(AttributeActionOverrides.blocked(singletonList( ArrayOverride.of( @@ -783,7 +784,115 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { .bcat(singletonList("cat3")) .bapp(singletonList("app3")) .btype(singletonMap("impId1", singletonList(3))) - .battr(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", singletonList(3)))) + .build())); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributesForVideo() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.videoBattrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( + Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .btype(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", singletonList(3)))) + .build())); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributesForAudio() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.audioBattrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( + Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .btype(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", singletonList(3)))) .build())); } @@ -1143,7 +1252,163 @@ public void responseBlockingConfigForShouldReturnResultWithMergedDealExceptionsW } @Test - public void responseBlockingConfigForShouldReturnAllAttributes() { + public void responseBlockingConfigForShouldReturnAllAttributesForBanner() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.bannerBattrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, Set.of("domain1.com", "domain2.com", "domain3.com"))) + .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) + .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) + .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, emptySet()))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributesForVideo() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.videoBattrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, Set.of("domain1.com", "domain2.com", "domain3.com"))) + .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) + .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) + .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, emptySet()))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributesForAudio() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -1188,7 +1453,7 @@ public void responseBlockingConfigForShouldReturnAllAttributes() { DealsConditions.of(singletonList("dealid1")), singletonList("app3"))))) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.audioBattrBuilder() .enforceBlocks(true) .allowedForDeals(asList(1, 2)) .actionOverrides(AttributeActionOverrides.response( @@ -1211,7 +1476,10 @@ public void responseBlockingConfigForShouldReturnAllAttributes() { .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) - .battr(BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)))) .build()); assertThat(result.getMessages()).isNull(); }); @@ -1240,10 +1508,15 @@ public void responseBlockingConfigForShouldReturnCattaxConfigDependsOnBcatConfig final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then + final Map> expectedBattr = new HashMap<>(); + expectedBattr.put(MediaType.BANNER, null); + expectedBattr.put(MediaType.VIDEO, null); + expectedBattr.put(MediaType.AUDIO, null); assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() .bcat(BidAttributeBlockingConfig.of(true, false, Set.of("cat1", "cat2", "cat3"))) .cattax(BidAttributeBlockingConfig.of(true, true, emptySet())) + .battr(expectedBattr) .build()); assertThat(result.getMessages()).isNull(); }); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java index 96d53a95f0a..79b1309a2ba 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java @@ -5,7 +5,12 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.response.Bid; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; @@ -16,6 +21,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.HashMap; import java.util.List; @@ -29,7 +35,12 @@ import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +@ExtendWith(MockitoExtension.class) public class BidsBlockerTest { private static final ObjectMapper mapper = new ObjectMapper() @@ -38,14 +49,18 @@ public class BidsBlockerTest { private static final OrtbVersion ORTB_VERSION = OrtbVersion.ORTB_2_5; + @Mock + private BidRejectionTracker bidRejectionTracker; + @Test public void shouldReturnEmptyResultWhenNoBlockingResponseConfig() { // given final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, null, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, null, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -55,12 +70,13 @@ public void shouldReturnEmptyResultWithErrorWhenInvalidAccountConfig() { .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.builder() .errors(singletonList("attributes field in account configuration is not an object")) .build()); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -70,10 +86,11 @@ public void shouldReturnEmptyResultWithoutErrorWhenInvalidAccountConfigAndDebugD .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.empty()); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -88,10 +105,11 @@ public void shouldReturnEmptyResultWhenBidWithoutAdomainAndBlockUnknownFalse() { // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -106,10 +124,11 @@ public void shouldReturnEmptyResultWhenBidWithoutAdomainAndEnforceBlocksFalseAnd // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -124,7 +143,7 @@ public void shouldReturnResultWithBidWhenBidWithoutAdomainAndBlockUnknownTrue() // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); @@ -142,10 +161,11 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndEnforceBlocksFals // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -160,10 +180,11 @@ public void shouldReturnEmptyResultWhenBidWithNotBlockedAdomain() { // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain2.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -178,10 +199,11 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndEnforceBlocksTr // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); } @Test @@ -195,17 +217,41 @@ public void shouldReturnEmptyResultWhenBidWithAdomainAndNoBlockedAttributes() { // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedVideoAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.videoBattrBuilder() .enforceBlocks(true) .build()) .build())); @@ -215,12 +261,36 @@ public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() .impid("impId2") .attr(singletonList(1)))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() - .battr(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", asList(1, 2)))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedAudioAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.audioBattrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -236,10 +306,11 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndInDealsExceptions // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -255,10 +326,11 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndNotInDealsExcep // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); } @Test @@ -273,7 +345,7 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -281,6 +353,7 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { assertThat(result.getDebugMessages()).containsOnly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [bcat]"); }); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); } @Test @@ -295,10 +368,11 @@ public void shouldReturnResultWithBidWithoutDebugMessageWhenBidIsBlockedAndDebug // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); } @Test @@ -314,7 +388,7 @@ public void shouldReturnResultWithAnalyticsResults() { .bapp(Attribute.bappBuilder() .enforceBlocks(true) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); @@ -339,9 +413,9 @@ public void shouldReturnResultWithAnalyticsResults() { .badv(asList("domain1.com", "domain2.com", "domain3.com")) .bcat(asList("cat1", "cat2", "cat3")) .bapp(asList("app1", "app2", "app3")) - .battr(singletonMap("impId2", asList(1, 2, 3))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId2", asList(1, 2, 3)))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -361,6 +435,11 @@ public void shouldReturnResultWithAnalyticsResults() { AnalyticsResult.of("success-blocked", analyticsResultValues2, "bidder1", "impId2"), AnalyticsResult.of("success-allow", null, "bidder1", "impId1")); }); + + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).reject("impId2", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verifyNoMoreInteractions(bidRejectionTracker); } @Test @@ -381,7 +460,7 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .enforceBlocks(true) .allowedForDeals(singletonList("app2")) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .allowedForDeals(singletonList(2)) .build()) @@ -409,9 +488,9 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .badv(asList("domain1.com", "domain2.com")) .bcat(asList("cat1", "cat2")) .bapp(asList("app1", "app2")) - .battr(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -423,6 +502,10 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { "Bid 5 from bidder bidder1 has been rejected, failed checks: [battr]", "Bid 7 from bidder bidder1 has been rejected, failed checks: [badv, bcat]"); }); + + verify(bidRejectionTracker, times(5)).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker, times(2)).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verifyNoMoreInteractions(bidRejectionTracker); } @Test @@ -443,12 +526,14 @@ public void shouldReturnEmptyResultForCattaxIfBidderSupportsLowerThan26() { bid(bid -> bid.cattax(3)), bid()); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()) .extracting(ExecutionResult::getValue) .isNull(); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -465,12 +550,14 @@ public void shouldPassBidIfCattaxIsNull() { final List bids = singletonList(bid()); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()) .extracting(ExecutionResult::getValue) .isNull(); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -489,7 +576,7 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { bid(bid -> bid.cattax(2))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().cattaxComplement(2).build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -497,6 +584,8 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { assertThat(result.getDebugMessages()).containsExactly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [cattax]"); }); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -515,7 +604,7 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { bid(bid -> bid.cattax(2))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -523,6 +612,8 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { assertThat(result.getDebugMessages()).containsExactly( "Bid 1 from bidder bidder1 has been rejected, failed checks: [cattax]"); }); + + verifyNoInteractions(bidRejectionTracker); } private static BidderBid bid() { diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java index be55ca645a1..1ba9ca80532 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java @@ -8,7 +8,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Video; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java index 25ceedd38c6..630c09e96d1 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java @@ -1,12 +1,16 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import org.junit.Test; +import com.iab.openrtb.request.Video; +import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; +import java.util.Map; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -138,10 +142,12 @@ public void shouldReplaceImpBtypeWhenAbsent() { } @Test - public void shouldReplaceImpBattrWhenAbsent() { + public void shouldReplaceImpBannerBattrWhenAbsent() { // given final RequestUpdater updater = RequestUpdater.create( - BlockedAttributes.builder().battr(singletonMap("impId1", asList(1, 2))).build()); + BlockedAttributes.builder() + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) + .build()); final BidRequest request = BidRequest.builder() .imp(singletonList(Imp.builder() .id("impId1") @@ -160,6 +166,56 @@ public void shouldReplaceImpBattrWhenAbsent() { .build()); } + @Test + public void shouldReplaceImpVideoBattrWhenAbsent() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", asList(1, 2)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldReplaceImpAudioBattrWhenAbsent() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", asList(1, 2)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .audio(Audio.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + @Test public void shouldNotChangeImpsWhenNoBlockedBannerTypeAndBlockedBannerAttr() { // given @@ -180,7 +236,43 @@ public void shouldNotChangeImpWhenNoBlockedBannerTypeAndBlockedBannerAttrForImp( final RequestUpdater updater = RequestUpdater.create( BlockedAttributes.builder() .btype(singletonMap("impId2", singletonList(1))) - .battr(singletonMap("impId2", singletonList(1))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId2", singletonList(1)))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedVideoAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId2", singletonList(1)))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedAudioAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId2", singletonList(1)))) .build()); final Imp imp = Imp.builder().build(); final BidRequest request = BidRequest.builder() @@ -198,7 +290,7 @@ public void shouldKeepImpBtypeWhenNoBlockedBannerTypeAndPresentBlockedBannerAttr // given final RequestUpdater updater = RequestUpdater.create( BlockedAttributes.builder() - .battr(singletonMap("impId1", singletonList(1))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", singletonList(1)))) .build()); final Imp imp = Imp.builder() .id("impId1") @@ -256,7 +348,10 @@ public void shouldUpdateAllAttributes() { .bcat(asList("cat1", "cat2")) .bapp(asList("app1", "app2")) .btype(singletonMap("impId1", asList(1, 2))) - .battr(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) .build()); final BidRequest request = BidRequest.builder() .imp(singletonList(Imp.builder().id("impId1").build())) @@ -273,6 +368,8 @@ public void shouldUpdateAllAttributes() { .btype(asList(1, 2)) .battr(asList(1, 2)) .build()) + .video(Video.builder().battr(asList(3, 4)).build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) .build())) .build()); } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java index 279201043c5..33da2c7bf4d 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java @@ -1,7 +1,7 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; import com.iab.openrtb.response.Bid; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; import org.prebid.server.proto.openrtb.ext.response.BidType; diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java index 6e7d3257a33..e60354670e9 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java @@ -63,8 +63,18 @@ public static AttributeBuilder btypeBuilder() { .field("banner-type"); } - public static AttributeBuilder battrBuilder() { + public static AttributeBuilder bannerBattrBuilder() { return Attribute.builder() .field("banner-attr"); } + + public static AttributeBuilder videoBattrBuilder() { + return Attribute.builder() + .field("video-attr"); + } + + public static AttributeBuilder audioBattrBuilder() { + return Attribute.builder() + .field("audio-attr"); + } } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java index 3c773aaed45..bff5a832ffc 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java @@ -1,6 +1,6 @@ package org.prebid.server.hooks.modules.ortb2.blocking.model; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index ae8d6f73bc5..fe1ea6e9614 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -9,12 +9,12 @@ import com.iab.openrtb.request.Video; import io.vertx.core.Future; import org.assertj.core.api.InstanceOfAssertFactories; -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.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; @@ -36,28 +36,32 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.spring.config.bidder.model.Ortb; +import java.util.Map; + import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +@ExtendWith(MockitoExtension.class) public class Ortb2BlockingBidderRequestHookTest { private static final ObjectMapper mapper = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private BidderCatalog bidderCatalog; + @Mock(strictness = Mock.Strictness.LENIENT) + private BidRejectionTracker bidRejectionTracker; + private Ortb2BlockingBidderRequestHook hook; - @Before + @BeforeEach public void setUp() { given(bidderCatalog.bidderInfoByName(anyString())) .willReturn(bidderInfo(OrtbVersion.ORTB_2_5)); @@ -67,10 +71,21 @@ public void setUp() { @Test public void shouldReturnResultWithNoActionWhenNoBlockingAttributes() { + // given + given(bidderCatalog.bidderInfoByName(anyString())) + .willReturn(bidderInfo(OrtbVersion.ORTB_2_6)); + given(bidderCatalog.bidderInfoByName(eq("bidder1Base"))) + .willReturn(bidderInfo(OrtbVersion.ORTB_2_5)); + // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", null, true)); + BidderInvocationContextImpl.of( + "bidder1", + Map.of("bidder1", "bidder1Base"), + bidRejectionTracker, + null, + true)); // then assertThat(result.succeeded()).isTrue(); @@ -90,7 +105,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -111,7 +126,7 @@ public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndD // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -137,7 +152,7 @@ public void shouldReturnResultWithModuleContextAndPayloadUpdate() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -189,7 +204,7 @@ public void shouldReturnResultWithUpdateActionAndWarning() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -226,7 +241,7 @@ public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -257,6 +272,7 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) { null, null, 0, + null, false, false, null, diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java index 7407d1d1ba4..0e0a6811835 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java @@ -6,7 +6,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.response.Bid; import io.vertx.core.Future; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; @@ -31,6 +36,7 @@ import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.response.BidType; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -39,6 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +@ExtendWith(MockitoExtension.class) public class Ortb2BlockingRawBidderResponseHookTest { private static final ObjectMapper mapper = new ObjectMapper() @@ -48,12 +55,15 @@ public class Ortb2BlockingRawBidderResponseHookTest { private final Ortb2BlockingRawBidderResponseHook hook = new Ortb2BlockingRawBidderResponseHook( ObjectMapperProvider.mapper()); + @Mock + private BidRejectionTracker bidRejectionTracker; + @Test public void shouldReturnResultWithNoActionWhenNoBidsBlocked() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", null, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, null, true)); // then assertThat(result.succeeded()).isTrue(); @@ -83,7 +93,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -104,7 +114,7 @@ public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndD // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -134,6 +144,9 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { BidderInvocationContextImpl.builder() .bidder("bidder1") .accountConfig(accountConfig) + .auctionContext(AuctionContext.builder() + .bidRejectionTrackers(Map.of("bidder1", bidRejectionTracker)) + .build()) .moduleContext(ModuleContext.create().with( "bidder1", BlockedAttributes.builder().badv(singletonList("domain2.com")).build())) .debugEnabled(true) @@ -211,7 +224,7 @@ public void shouldReturnResultWithUpdateActionAndWarning() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -245,7 +258,7 @@ public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -271,7 +284,7 @@ public void shouldReturnResultWithUpdateActionAndDebugMessage() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -297,7 +310,7 @@ public void shouldReturnResultWithUpdateActionAndNoDebugMessageWhenDebugDisabled // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java index 99d19db08da..8b68c9279df 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java @@ -1,13 +1,19 @@ package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; import lombok.Builder; import lombok.Value; import lombok.experimental.Accessors; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.execution.Timeout; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.model.Endpoint; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Map; @Accessors(fluent = true) @Builder @@ -28,9 +34,38 @@ public class BidderInvocationContextImpl implements BidderInvocationContext { String bidder; - public static BidderInvocationContext of(String bidder, ObjectNode accountConfig, boolean debugEnabled) { + public static BidderInvocationContext of(String bidder, + BidRejectionTracker bidRejectionTracker, + ObjectNode accountConfig, + boolean debugEnabled) { + + return BidderInvocationContextImpl.builder() + .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) + .build()) + .accountConfig(accountConfig) + .debugEnabled(debugEnabled) + .build(); + } + + public static BidderInvocationContext of(String bidder, + Map aliases, + BidRejectionTracker bidRejectionTracker, + ObjectNode accountConfig, + boolean debugEnabled) { + return BidderInvocationContextImpl.builder() .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(aliases) + .build())) + .build()) + .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) + .build()) .accountConfig(accountConfig) .debugEnabled(debugEnabled) .build(); diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml new file mode 100644 index 00000000000..802bed7fe04 --- /dev/null +++ b/extra/modules/pb-response-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.15.0-SNAPSHOT + + + pb-response-correction + + pb-response-correction + Response correction module + diff --git a/extra/modules/pb-response-correction/src/lombok.config b/extra/modules/pb-response-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-response-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..e119bb59703 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.pb.response.correction.config; + +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrection; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.v1.ResponseCorrectionModule; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + ResponseCorrectionModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class ResponseCorrectionModuleConfiguration { + + @Bean + AppVideoHtmlCorrectionProducer appVideoHtmlCorrectionProducer( + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new AppVideoHtmlCorrectionProducer( + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), logSamplingRate)); + } + + @Bean + ResponseCorrectionProvider responseCorrectionProvider(List correctionProducers) { + return new ResponseCorrectionProvider(correctionProducers); + } + + @Bean + ResponseCorrectionModule responseCorrectionModule(ResponseCorrectionProvider responseCorrectionProvider) { + return new ResponseCorrectionModule(responseCorrectionProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java new file mode 100644 index 00000000000..9bdf2ceea8c --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class ResponseCorrectionProvider { + + private final List correctionProducers; + + public ResponseCorrectionProvider(List correctionProducers) { + this.correctionProducers = Objects.requireNonNull(correctionProducers); + } + + public List corrections(Config config, BidRequest bidRequest) { + return correctionProducers.stream() + .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) + .map(CorrectionProducer::produce) + .toList(); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java new file mode 100644 index 00000000000..06b0990f149 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AppVideoHtmlConfig { + + boolean enabled; + + @JsonProperty("excluded-bidders") + List excludedBidders; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java new file mode 100644 index 00000000000..17cd2453b16 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Config { + + boolean enabled; + + @JsonProperty("app-video-html") + AppVideoHtmlConfig appVideoHtmlConfig; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java new file mode 100644 index 00000000000..3f7abf1c5c5 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +import java.util.List; + +public interface Correction { + + List apply(Config config, List bidderResponses); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..6cd19836b96 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java new file mode 100644 index 00000000000..5b3ee918e86 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java @@ -0,0 +1,138 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +public class AppVideoHtmlCorrection implements Correction { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(AppVideoHtmlCorrection.class)); + + private static final Pattern VAST_XML_PATTERN = Pattern.compile(".*<\\s*VAST\\s+.*", Pattern.CASE_INSENSITIVE); + private static final TypeReference> EXT_BID_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String NATIVE_ADM_MESSAGE = "Bid %s of bidder %s has an JSON ADM, that appears to be native"; + private static final String ADM_WITH_NO_ASSETS_MESSAGE = "Bid %s of bidder %s has a JSON ADM, but without assets"; + private static final String CHANGING_BID_MEDIA_TYPE_MESSAGE = "Bid %s of bidder %s: changing media type to banner"; + + private final ObjectMapper mapper; + private final double logSamplingRate; + + public AppVideoHtmlCorrection(ObjectMapper mapper, double logSamplingRate) { + this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; + } + + @Override + public List apply(Config config, List bidderResponses) { + final Collection excludedBidders = CollectionUtils.emptyIfNull( + config.getAppVideoHtmlConfig().getExcludedBidders()); + + return bidderResponses.stream() + .map(response -> modify(response, excludedBidders)) + .toList(); + } + + private BidderResponse modify(BidderResponse response, Collection excludedBidders) { + final String bidder = response.getBidder(); + if (excludedBidders.contains(bidder)) { + return response; + } + + final BidderSeatBid seatBid = response.getSeatBid(); + final List modifiedBids = seatBid.getBids().stream() + .map(bidderBid -> modifyBid(bidder, bidderBid)) + .toList(); + + return response.with(seatBid.with(modifiedBids)); + } + + private BidderBid modifyBid(String bidder, BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidId = bid.getId(); + final String adm = bid.getAdm(); + + if (adm == null || isVideoWithVastXml(bidderBid.getType(), adm) || hasNativeAdm(adm, bidId, bidder)) { + return bidderBid; + } + + conditionalLogger.warn(CHANGING_BID_MEDIA_TYPE_MESSAGE.formatted(bidId, bidder), logSamplingRate); + + final ExtBidPrebid prebid = parseExtBidPrebid(bid); + + final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .mediaType(BidType.video.getName()) + .build(); + + final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid) + .map(ExtBidPrebid::toBuilder) + .orElseGet(ExtBidPrebid::builder) + .meta(modifiedMeta) + .type(BidType.banner) + .build(); + + final ObjectNode modifiedBidExt = mapper.valueToTree(ExtPrebid.of(modifiedPrebid, null)); + + return bidderBid.toBuilder() + .type(BidType.banner) + .bid(bid.toBuilder().ext(modifiedBidExt).build()) + .build(); + } + + private boolean hasNativeAdm(String adm, String bidId, String bidder) { + final JsonNode admNode; + try { + admNode = mapper.readTree(adm); + } catch (JsonProcessingException e) { + return false; + } + + final boolean hasAssets = admNode.has("assets"); + final String warningMessage = hasAssets + ? NATIVE_ADM_MESSAGE.formatted(bidId, bidder) + : ADM_WITH_NO_ASSETS_MESSAGE.formatted(bidId, bidder); + + conditionalLogger.warn(warningMessage, logSamplingRate); + return hasAssets; + } + + private static boolean isVideoWithVastXml(BidType type, String adm) { + return type == BidType.video && VAST_XML_PATTERN.matcher(adm).matches(); + } + + private ExtBidPrebid parseExtBidPrebid(Bid bid) { + try { + return mapper.convertValue(bid.getExt(), EXT_BID_PREBID_TYPE_REFERENCE).getPrebid(); + } catch (Exception e) { + return null; + } + } + +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java new file mode 100644 index 00000000000..f7a05137bf0 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +public class AppVideoHtmlCorrectionProducer implements CorrectionProducer { + + private final AppVideoHtmlCorrection correctionInstance; + + public AppVideoHtmlCorrectionProducer(AppVideoHtmlCorrection correction) { + this.correctionInstance = correction; + } + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final AppVideoHtmlConfig appVideoHtmlConfig = config.getAppVideoHtmlConfig(); + final boolean enabled = appVideoHtmlConfig != null && appVideoHtmlConfig.isEnabled(); + return enabled && bidRequest.getApp() != null; + } + + @Override + public Correction produce() { + return correctionInstance; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java new file mode 100644 index 00000000000..09e4640b064 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -0,0 +1,105 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.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.bidder.AllProcessedBidResponsesHook; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; +import java.util.Objects; + +public class ResponseCorrectionAllProcessedBidResponsesHook implements AllProcessedBidResponsesHook { + + private static final String CODE = "pb-response-correction-all-processed-bid-responses"; + + private final ResponseCorrectionProvider responseCorrectionProvider; + private final ObjectMapper mapper; + + public ResponseCorrectionAllProcessedBidResponsesHook(ResponseCorrectionProvider responseCorrectionProvider, + ObjectMapper mapper) { + this.responseCorrectionProvider = Objects.requireNonNull(responseCorrectionProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AllProcessedBidResponsesPayload payload, + AuctionInvocationContext context) { + + final Config config; + try { + config = moduleConfig(context.accountConfig()); + } catch (PreBidException e) { + return failure(e.getMessage()); + } + + if (config == null || !config.isEnabled()) { + return noAction(); + } + + final BidRequest bidRequest = context.auctionContext().getBidRequest(); + + final List corrections = responseCorrectionProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AllProcessedBidResponsesPayloadImpl.of( + applyCorrections(initialPayload.bidResponses(), config, corrections))) + .build(); + + return Future.succeededFuture(invocationResult); + } + + private Config moduleConfig(ObjectNode accountConfig) { + try { + return mapper.treeToValue(accountConfig, Config.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static List applyCorrections(List bidderResponses, Config config, List corrections) { + List result = bidderResponses; + for (Correction correction : corrections) { + result = correction.apply(config, result); + } + return result; + } + + private Future> failure(String message) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .action(InvocationAction.no_action) + .build()); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java new file mode 100644 index 00000000000..29e32743201 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +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; + +public class ResponseCorrectionModule implements Module { + + public static final String CODE = "pb-response-correction"; + + private final Collection> hooks; + + public ResponseCorrectionModule(ResponseCorrectionProvider responseCorrectionProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new ResponseCorrectionAllProcessedBidResponsesHook(responseCorrectionProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..1a39413583c --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +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 org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Object moduleContext; + + Tags analyticsTags; +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java new file mode 100644 index 00000000000..6b8fc33ba95 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ResponseCorrectionProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private ResponseCorrectionProvider target; + + @BeforeEach + public void setUp() { + target = new ResponseCorrectionProvider(singletonList(correctionProducer)); + } + + @Test + public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(false); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).isEmpty(); + } + + @Test + public void correctionsShouldReturnProducedCorrection() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(true); + + final Correction correction = mock(Correction.class); + given(correctionProducer.produce()).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java new file mode 100644 index 00000000000..15305d6bed2 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class AppVideoHtmlCorrectionProducerTest { + + private final AppVideoHtmlCorrection CORRECTION_INSTANCE = + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), 0.1); + + private final AppVideoHtmlCorrectionProducer target = new AppVideoHtmlCorrectionProducer(CORRECTION_INSTANCE); + + @Test + public void produceShouldReturnCorrectionInstance() { + // when & then + assertThat(target.produce()).isSameAs(CORRECTION_INSTANCE); + } + + @Test + public void shouldProduceReturnFalseWhenAppVideoHtmlConfigIsDisabled() { + // given + final Config givenConfig = givenConfig(false); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnFalseWhenBidRequestIsNotAppRequest() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().site(Site.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnTrueWhenConfigIsEnabledAndBidRequestHasApp() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isTrue(); + } + + private static Config givenConfig(boolean enabled) { + return Config.of(true, AppVideoHtmlConfig.of(enabled, null)); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java new file mode 100644 index 00000000000..9b36b0210da --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java @@ -0,0 +1,197 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AppVideoHtmlCorrectionTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + private final AppVideoHtmlCorrection target = new AppVideoHtmlCorrection(MAPPER, 0.1); + + @Test + public void applyShouldNotChangeBidResponsesFromExcludedBidders() { + // given + final Config givenConfig = givenConfig(List.of("bidderA", "bidderB")); + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", null, 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static Config givenConfig(List excludedBidders) { + return Config.of(true, AppVideoHtmlConfig.of(true, excludedBidders)); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenAdmIsNull() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + final BidderBid givenBid = givenBid(null, BidType.video); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of(List.of(givenBid)), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static BidderBid givenBid(String adm, BidType type) { + return givenBid(adm, type, null); + } + + private static BidderBid givenBid(String adm, BidType type, ObjectNode bidExt) { + final Bid bid = Bid.builder().adm(adm).ext(bidExt).build(); + return BidderBid.of(bid, type, "USD"); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidIsVideoAndHasVastXmlInAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid("< \tVAST anything>", BidType.video))), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidHasNativeAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid("{\"field\":1,\"assets\":[{\"id\":2}]}", BidType.video))), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsJsonButNotNative() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.video))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + private static Config givenConfig() { + return Config.of(true, AppVideoHtmlConfig.of(true, null)); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsVastXmlAndTypeIsNotVideo() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.xNative))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndOverwriteMetaTypeToVideoWhenAdmIsNotVastXmlAndTypeIsVideo() { + // given + final Config givenConfig = givenConfig(); + + final ExtBidPrebid givenPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("banner").build()) + .build(); + final ObjectNode givenBidExt = MAPPER.valueToTree(ExtPrebid.of(givenPrebid, null)); + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.video, givenBidExt))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java new file mode 100644 index 00000000000..0e525b06f24 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java @@ -0,0 +1,118 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +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.bidder.AllProcessedBidResponsesPayload; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ResponseCorrectionAllProcessedBidResponsesHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private ResponseCorrectionProvider responseCorrectionProvider; + + private ResponseCorrectionAllProcessedBidResponsesHook target; + + @Mock + private AllProcessedBidResponsesPayload payload; + + @Mock(strictness = Mock.Strictness.LENIENT) + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(true, null))); + given(invocationContext.auctionContext()) + .willReturn(AuctionContext.builder().bidRequest(BidRequest.builder().build()).build()); + + target = new ResponseCorrectionAllProcessedBidResponsesHook(responseCorrectionProvider, MAPPER); + } + + @Test + public void callShouldReturnFailedResultOnInvalidConfiguration() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList()))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type"); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionOnDisabledConfig() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(false, null))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { + // given + given(responseCorrectionProvider.corrections(any(), any())).willReturn(emptyList()); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnUpdate() { + // given + final Correction correction = mock(Correction.class); + given(responseCorrectionProvider.corrections(any(), any())).willReturn(singletonList(correction)); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + }); + } +} diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index d5f8b4059b4..fc852b520f2 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 2.13.0-SNAPSHOT + 3.15.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java index 39c3a4f6d49..e528ce69c4e 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java @@ -2,6 +2,8 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -14,8 +16,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; public class BidResponsesMraidFilter { @@ -23,7 +23,10 @@ public class BidResponsesMraidFilter { private static final String TAG_STATUS = "success-block"; private static final Map TAG_VALUES = Map.of("richmedia-format", "mraid"); - public MraidFilterResult filterByPattern(String mraidScriptPattern, List responses) { + public MraidFilterResult filterByPattern(String mraidScriptPattern, + List responses, + Map bidRejectionTrackers) { + List filteredResponses = new ArrayList<>(); List analyticsResults = new ArrayList<>(); @@ -43,18 +46,21 @@ public MraidFilterResult filterByPattern(String mraidScriptPattern, List errors = new ArrayList<>(seatBid.getErrors()); - errors.add(BidderError.of( - "Invalid creatives", - BidderError.Type.invalid_creative, - new HashSet<>(rejectedImps))); + errors.add(BidderError.of("Invalid bid", BidderError.Type.invalid_bid, new HashSet<>(rejectedImps))); filteredResponses.add(bidderResponse.with(seatBid.with(validBids, errors))); } } diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/PbRichMediaFilterProperties.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/PbRichMediaFilterProperties.java index e22419d1702..5cd1a154012 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/PbRichMediaFilterProperties.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/PbRichMediaFilterProperties.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; -import javax.validation.constraints.NotBlank; - @Value(staticConstructor = "of") public class PbRichMediaFilterProperties { diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java index 3465eb08fc4..ee92a5e2064 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java @@ -58,7 +58,10 @@ public Future> call( final List responses = allProcessedBidResponsesPayload.bidResponses(); if (BooleanUtils.isTrue(properties.getFilterMraid())) { - final MraidFilterResult filterResult = mraidFilter.filterByPattern(properties.getMraidScriptPattern(), responses); + final MraidFilterResult filterResult = mraidFilter.filterByPattern( + properties.getMraidScriptPattern(), + responses, + auctionInvocationContext.auctionContext().getBidRejectionTrackers()); final InvocationAction action = filterResult.hasRejectedBids() ? InvocationAction.update : InvocationAction.no_action; diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java index e74ca82c603..f2066474df9 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java @@ -1,8 +1,9 @@ package org.prebid.server.hooks.modules.pb.richmedia.filter.core; import com.iab.openrtb.response.Bid; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -15,6 +16,10 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class BidResponsesMraidFilterTest { @@ -25,14 +30,21 @@ public void filterShouldReturnOriginalBidsWhenNoBidsHaveMraidScriptInAdm() { // given final BidderResponse responseA = givenBidderResponse("bidderA", List.of(givenBid("imp_id", "adm1"))); final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid("imp_id", "adm2"))); + final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class); + final Map givenTrackers = Map.of( + "bidderA", bidRejectionTrackerA, + "bidderB", bidRejectionTrackerB); // when - final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB)); + final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB), givenTrackers); // then assertThat(filterResult.getFilterResult()).containsExactly(responseA, responseB); assertThat(filterResult.getAnalyticsResult()).isEmpty(); assertThat(filterResult.hasRejectedBids()).isFalse(); + + verifyNoInteractions(bidRejectionTrackerA, bidRejectionTrackerB); } @Test @@ -48,10 +60,19 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() givenBid("imp_id1", "adm1_mraid.js"), givenBid("imp_id2", "adm2_mraid.js"))); + final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerC = mock(BidRejectionTracker.class); + final Map givenTrackers = Map.of( + "bidderA", bidRejectionTrackerA, + "bidderB", bidRejectionTrackerB, + "bidderC", bidRejectionTrackerC); + // when final MraidFilterResult filterResult = target.filterByPattern( "mraid.js", - List.of(responseA, responseB, responseC)); + List.of(responseA, responseB, responseC), + givenTrackers); // then final BidderResponse expectedResponseA = givenBidderResponse( @@ -82,6 +103,13 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() assertThat(filterResult.getAnalyticsResult()) .containsExactlyInAnyOrder(expectedAnalyticsResultB, expectedAnalyticsResultC); assertThat(filterResult.hasRejectedBids()).isTrue(); + + verifyNoInteractions(bidRejectionTrackerA); + verify(bidRejectionTrackerB) + .reject(List.of("imp_id2"), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTrackerC) + .reject(List.of("imp_id1", "imp_id2"), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verifyNoMoreInteractions(bidRejectionTrackerB, bidRejectionTrackerC); } private static BidderResponse givenBidderResponse(String bidder, List bids) { @@ -97,7 +125,7 @@ private static BidderBid givenBid(String impId, String adm) { } private static BidderError givenError(String... rejectedImps) { - return BidderError.of("Invalid creatives", BidderError.Type.invalid_creative, Set.of(rejectedImps)); + return BidderError.of("Invalid bid", BidderError.Type.invalid_bid, Set.of(rejectedImps)); } } diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolverTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolverTest.java index 8be4aa2c29f..6cf0be0612c 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolverTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolverTest.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; import org.prebid.server.json.ObjectMapperProvider; @@ -22,7 +22,7 @@ public class ModuleConfigResolverTest { private ModuleConfigResolver target; - @Before + @BeforeEach public void before() { target = new ModuleConfigResolver(OBJECT_MAPPER, GLOBAL_PROPERTIES); } diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java index 25970303878..47d5ab27253 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java @@ -2,12 +2,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; 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.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; @@ -36,35 +37,39 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) public class PbRichmediaFilterAllProcessedBidResponsesHookTest { private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock private AllProcessedBidResponsesPayload allProcessedBidResponsesPayload; - @Mock + @Mock(strictness = LENIENT) private AuctionInvocationContext auctionInvocationContext; @Mock private BidResponsesMraidFilter mraidFilter; - @Mock + @Mock(strictness = LENIENT) private ModuleConfigResolver configResolver; private PbRichmediaFilterAllProcessedBidResponsesHook target; - @Before + private static final Map BID_REJECTION_TRACKERS = Map.of( + "bidder", new BidRejectionTracker("bidder", Collections.emptySet(), 0.1)); + + @BeforeEach public void setUp() { target = new PbRichmediaFilterAllProcessedBidResponsesHook(ObjectMapperProvider.mapper(), mraidFilter, configResolver); when(configResolver.resolve(any())).thenReturn(PbRichMediaFilterProperties.of(true, "pattern")); + when(auctionInvocationContext.auctionContext()) + .thenReturn(AuctionContext.builder().bidRejectionTrackers(BID_REJECTION_TRACKERS).build()); } @Test @@ -105,7 +110,7 @@ public void callShouldReturnResultWithUpdateActionWhenSomeResponsesWereFilteredO // given final List givenResponses = givenBidderResponses(2); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, List.of(givenAnalyticsResult("bidder", "imp_id")))); // when @@ -128,7 +133,7 @@ public void callShouldReturnResultWithNoActionWhenNothingWereFilteredOut() { // given final List givenResponses = givenBidderResponses(2); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, Collections.emptyList())); // when @@ -152,7 +157,7 @@ public void callShouldReturnResultOfFilteredResponses() { final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); final List expectedResponses = givenBidderResponses(2); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(expectedResponses, Collections.emptyList())); // when @@ -176,7 +181,7 @@ public void callShouldReturnAnalyticsResultsOfRejectedBids() { // given final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of( givenResponses, List.of( @@ -221,7 +226,7 @@ public void callShouldReturnEmptyAnalyticsResultsWhenThereAreNoRejectedBids() { // given final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, Collections.emptyList())); // when diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterModuleTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterModuleTest.java index 0ebed7919eb..9146788e0c1 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterModuleTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterModuleTest.java @@ -1,6 +1,6 @@ package org.prebid.server.hooks.modules.pb.richmedia.filter.v1; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 32f53d9ae05..700902c8834 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 2.13.0-SNAPSHOT + 3.15.0-SNAPSHOT ../../extra/pom.xml @@ -20,57 +20,16 @@ ortb2-blocking confiant-ad-quality pb-richmedia-filter + fiftyone-devicedetection + pb-response-correction - - UTF-8 - UTF-8 - 17 - ${java.version} - ${java.version} - - 5.9.0 - 3.23.1 - 4.13.2 - 4.7.0 - - 3.10.1 - 2.22.2 - - org.prebid prebid-server - 2.13.0-SNAPSHOT - - - org.projectlombok - lombok - ${lombok.version} - - - org.junit - junit-bom - ${junit-bom.version} - pom - import - - - org.assertj - assertj-core - ${assertj.version} - - - junit - junit - ${junit.version} - - - org.mockito - mockito-core - ${mockito.version} + ${project.version} @@ -81,8 +40,8 @@ prebid-server - org.junit.vintage - junit-vintage-engine + org.junit.jupiter + junit-jupiter-engine test @@ -91,13 +50,13 @@ test - junit - junit + org.mockito + mockito-core test org.mockito - mockito-core + mockito-junit-jupiter test @@ -114,6 +73,9 @@ org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} + + ${skipUnitTests} + diff --git a/extra/pom.xml b/extra/pom.xml index dd0ac70cacc..6fe748f904a 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server-aggregator - 2.13.0-SNAPSHOT + 3.15.0-SNAPSHOT pom @@ -15,12 +15,55 @@ + + 21 UTF-8 - UTF-8 - 17 - 17 - 1.18.24 - 3.0.0-M6 + UTF-8 + ${java.version} + ${java.version} + + + 3.1.0 + 3.13.0 + 3.3.0 + ${maven-surefire-plugin.version} + 0.8.12 + 0.44.0 + + + 3.2.3 + 4.5.5 + 2.0.1.Final + 4.4 + 1.26.0 + 3.6.1 + 2.1 + 4.5.14 + 5.3.1 + 6.4.5 + 1.0.76 + 1.13 + 2.2.0 + 1.2.2 + 0.16.0 + 2.0.10 + 3.2.0 + 2.12.0 + 3.25.5 + ${protobuf.version} + 1.0.7 + 2.26.24 + + + 3.9.1 + 2.4-M4-groovy-4.0 + + 5.15.0 + + + false + false + true @@ -29,11 +72,191 @@ bundle + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + io.vertx + vertx-dependencies + ${vertx.version} + pom + import + + + org.spockframework + spock-bom + ${spock.version} + pom + import + + + org.wiremock + wiremock-jetty12 + ${wiremock.version} + + + javax.validation + validation-api + ${validation-api.version} + + + com.ongres.scram + client + ${scram.version} + + + org.apache.commons + commons-collections4 + ${commons.collections.version} + + + org.apache.commons + commons-compress + ${commons.compress.version} + + + org.apache.commons + commons-math3 + ${commons-math3.version} + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + com.github.seancfoley + ipaddress + ${ipaddress.version} + + + com.github.oshi + oshi-core + ${oshi.version} + + + com.networknt + json-schema-validator + ${json-schema-validator.version} + + + com.github.java-json-tools + json-patch + ${jsonpatch.version} + + + com.google.code.findbugs + jsr305 + + + + + de.malkusch.whois-server-list + public-suffix-list + ${psl.version} + + + com.google.code.findbugs + jsr305 + + + + + com.izettle + dropwizard-metrics-influxdb + ${metrics-influxdb.version} + + + io.dropwizard + dropwizard-metrics + + + org.apache.kafka + kafka-clients + + + + + io.prometheus + simpleclient_vertx4 + ${vertx.prometheus.version} + + + io.prometheus + simpleclient_dropwizard + ${vertx.prometheus.version} + + + com.iabtcf + iabtcf-decoder + ${iabtcf.version} + + + com.iabtcf + iabtcf-encoder + ${iabtcf.version} + + + com.iabgpp + iabgpp-encoder + ${gpp-encoder.version} + + + com.maxmind.geoip2 + geoip2 + ${maxmind-client.version} + + + software.amazon.awssdk + s3 + ${aws.awssdk.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + + + com.google.code.findbugs + jsr305 + + + + + io.github.jamsesso + json-logic-java + ${json-logic.version} + + + org.mock-server + mockserver-client-java + ${mockserver.version} + + + com.google.code.findbugs + jsr305 + + + + + + org.projectlombok lombok - ${lombok.version} provided @@ -45,10 +268,32 @@ maven-release-plugin ${maven-release-plugin.version} + false + true @{project.version} Prebid Server + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + before-unit-test-execution + + prepare-agent + + + + after-unit-test-execution + prepare-package + + report + + + + diff --git a/pom.xml b/pom.xml index 4cca09abf73..e674293243e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 2.13.0-SNAPSHOT + 3.15.0-SNAPSHOT extra/pom.xml @@ -19,131 +19,32 @@ - UTF-8 - UTF-8 - 17 - ${java.version} - ${java.version} Dockerfile - - 2.5.6 - 2.0.1.Final - 3.9.10 - 3.14.0 - 4.4 - 1.26.0 - 3.6.1 - 4.5.14 - 5.3.1 - 6.4.5 - 2.14.1 - 1.0.76 - 1.13 - 8.0.28 - 42.7.2 - 2.2.0 - 1.2.2 - 2.0.2 - 2.0.7 - 2.12.0 - 1.2.13 - 5.0.1 - 3.0.10 - 3.21.7 - 3.17.3 - 1.0.7 - 1.7.32 - - - 4.13.2 - 5.9.2 - 4.11.0 - 3.24.2 - 2.35.1 - 4.2.0 - 9.4.53.v20231009 - 4.4.0 - 2.2.220 - 2.4-M1-groovy-3.0 - 3.0.14 - 1.17.4 - 5.14.0 - 2.19.0 - 1.9.9.1 - 1.12.14 - - 3.1.2 - 10.3 - 1.2.0 - 0.8.7 - 2.2.4 - 3.10.1 - 2.22.2 - ${maven-surefire-plugin.version} - 0.40.2 - 1.13.1 - 2.10.0 - false - true - false - 1.6.2 - 3.0.0 + 3.4.0 + 10.17.0 + 1.9.0 + 4.9.10 + + 3.0.2 + 1.7.1 + 3.6.0 0.6.1 - - - - org.springframework.boot - spring-boot-dependencies - ${spring.boot.version} - pom - import - - - com.google.code.gson - gson - - - commons-codec - commons-codec - compile - - - org.apache.httpcomponents - httpcore - compile - - - com.rabbitmq - amqp-client - - - org.jboss.logging - jboss-logging - - - - org.springframework.boot spring-boot-starter - org.springframework.boot - spring-boot-starter-aop - - - javax.annotation - javax.annotation-api + jakarta.annotation + jakarta.annotation-api javax.validation validation-api - ${validation-api.version} org.hibernate.validator @@ -152,134 +53,126 @@ io.vertx vertx-core - ${vertx.version} io.vertx vertx-web - ${vertx.version} io.vertx vertx-config - ${vertx.version} io.vertx - vertx-jdbc-client - ${vertx.version} + vertx-mysql-client + + + io.vertx + vertx-pg-client io.vertx vertx-circuit-breaker - ${vertx.version} io.vertx vertx-dropwizard-metrics - ${vertx.version} io.vertx vertx-auth-common - ${vertx.version} + + + com.ongres.scram + client io.netty netty-transport-native-epoll linux-x86_64 + + io.netty + netty-transport-native-epoll + linux-aarch_64 + org.apache.commons commons-lang3 - ${commons.version} org.apache.commons commons-collections4 - ${commons.collections.version} org.apache.commons commons-compress - ${commons.compress.version} org.apache.commons commons-math3 - ${commons-math3.version} org.apache.httpcomponents httpclient - ${httpclient.version} com.github.seancfoley ipaddress - ${ipaddress.version} com.github.oshi oshi-core - ${oshi.version} com.fasterxml.jackson.core jackson-core - ${jackson.version} com.fasterxml.jackson.core jackson-databind - ${jackson.version} com.fasterxml.jackson.core jackson-annotations - ${jackson.version} com.fasterxml.jackson.dataformat jackson-dataformat-yaml - ${jackson.version} com.fasterxml.jackson.module jackson-module-blackbird - ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - ${jackson.version} test com.fasterxml.jackson.dataformat jackson-dataformat-xml - ${jackson.version} test com.networknt json-schema-validator - ${json-schema-validator.version} com.github.java-json-tools json-patch - ${jsonpatch.version} - mysql - mysql-connector-java - ${mysql.version} + com.mysql + mysql-connector-j org.postgresql postgresql - ${postgresql.version} + + + software.amazon.awssdk + s3 com.github.ben-manes.caffeine @@ -288,13 +181,6 @@ de.malkusch.whois-server-list public-suffix-list - ${psl.version} - - - com.google.code.findbugs - jsr305 - - io.dropwizard.metrics @@ -311,31 +197,14 @@ com.izettle dropwizard-metrics-influxdb - ${metrics-influxdb.version} - - - io.dropwizard - dropwizard-metrics - - - org.apache.kafka - kafka-clients - - - - - com.conversantmedia.gdpr - consent-string-sdk-java - ${consent-string-sdk.version} com.iabtcf iabtcf-decoder - ${iabtcf.version} io.prometheus - simpleclient_vertx + simpleclient_vertx4 io.prometheus @@ -344,92 +213,42 @@ com.maxmind.geoip2 geoip2 - ${maxmind-client.version} - - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.apache.logging.log4j - log4j-to-slf4j - - - org.apache.logging.log4j - log4j-api - - - com.zaxxer - HikariCP - ${hikari.version} com.iabgpp iabgpp-encoder - ${gpp-encoder.version} com.google.protobuf protobuf-java-util - ${protobuf.version} com.google.protobuf protobuf-java - ${protobuf.version} io.github.jamsesso json-logic-java - ${json-logic.version} - junit - junit - ${junit.version} - test - - - org.junit.vintage - junit-vintage-engine - ${junit-jupiter.version} + org.junit.jupiter + junit-jupiter-engine test org.mockito mockito-core - ${mockito.version} test org.mockito mockito-junit-jupiter - ${mockito.version} test org.assertj assertj-core - ${assertj.version} - test - - - org.awaitility - awaitility - ${awaitility.version} test @@ -439,114 +258,51 @@ io.vertx - vertx-unit - ${vertx.version} + vertx-junit5 test - com.github.tomakehurst - wiremock-jre8 - ${wiremock.version} + org.wiremock + wiremock-jetty12 test com.iabtcf iabtcf-encoder - ${iabtcf.version} - test - - - org.eclipse.jetty - jetty-server - ${jetty.version} - test - - - org.eclipse.jetty - jetty-servlet - ${jetty.version} - test - - - org.eclipse.jetty - jetty-servlets - ${jetty.version} test - org.eclipse.jetty - jetty-webapp - ${jetty.version} - test - - - org.eclipse.jetty - jetty-http - ${jetty.version} - test - - - org.eclipse.jetty - jetty-io - ${jetty.version} - test - - - org.eclipse.jetty - jetty-security - ${jetty.version} - test - - - org.eclipse.jetty - jetty-continuation - ${jetty.version} - test - - - org.eclipse.jetty - jetty-util - ${jetty.version} - test - - - org.eclipse.jetty - jetty-xml - ${jetty.version} + io.rest-assured + rest-assured test io.rest-assured - rest-assured - ${restassured.version} + json-path test - net.bytebuddy - byte-buddy - ${bytebuddy.version} + io.rest-assured + xml-path test org.spockframework spock-core - ${spock.version} test - org.codehaus.groovy + org.apache.groovy groovy - ${groovy.version} test - org.codehaus.groovy + org.apache.groovy groovy-yaml - ${groovy.version} test - org.hibernate + org.hibernate.orm hibernate-core test @@ -554,43 +310,41 @@ io.vertx vertx-codegen - ${vertx.version} test com.h2database h2 - ${h2.version} test org.testcontainers testcontainers - ${testcontainers.version} test org.testcontainers mockserver - ${testcontainers.version} test org.testcontainers mysql - ${testcontainers.version} test - org.mock-server - mockserver-client-java - ${mockserver.version} + org.testcontainers + localstack + test + + + org.testcontainers + postgresql test - io.qameta.allure - allure-java-commons - ${allure.version} + org.mock-server + mockserver-client-java test @@ -603,6 +357,11 @@ org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} + + + -parameters + + org.apache.maven.plugins @@ -727,7 +486,9 @@ ${project.basedir}/src/main/proto ${project.basedir}/src/test/proto - com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + + com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + @@ -742,7 +503,9 @@ wget - https://raw.githubusercontent.com/IABTechLab/openrtb-proto-v2/master/openrtb-core/src/main/protobuf/openrtb.proto + + https://raw.githubusercontent.com/IABTechLab/openrtb-proto-v2/master/openrtb-core/src/main/protobuf/openrtb.proto + ${project.basedir}/src/main/proto openrtb.proto @@ -754,7 +517,9 @@ wget - https://raw.githubusercontent.com/MagniteEngineering/xapi-proto/main/src/proto/com/magnite/openrtb/v2/openrtb-xapi.proto + + https://raw.githubusercontent.com/MagniteEngineering/xapi-proto/main/src/proto/com/magnite/openrtb/v2/openrtb-xapi.proto + ${project.basedir}/src/main/proto openrtb-xapi.proto @@ -776,49 +541,18 @@ org.jacoco jacoco-maven-plugin - ${jacoco-plugin.version} + ${skipUnitTests} com/iab/openrtb/** + com/iabtechlab/openrtb/** + com/magnite/openrtb/** **/proto/** **/model/** + **/functional/** org/prebid/server/spring/config/** - - - prepare-agent - - prepare-agent - - - - report - - report - - - - check - - check - - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 90% - - - - - - - pl.project13.maven @@ -908,14 +642,10 @@ org.apache.maven.plugins maven-failsafe-plugin - - -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" - - target/allure-results ${mockserver.version} ${project.version} - 2 + 5 false @@ -948,25 +678,13 @@ - - - org.aspectj - aspectjweaver - ${aspectj.version} - - - - - io.qameta.allure - allure-maven - ${allure-maven.version} org.apache.maven.plugins maven-compiler-plugin - 17 - 17 + ${java.version} + ${java.version} @@ -985,17 +703,6 @@ - - org.jacoco - jacoco-maven-plugin - - - - report - - - - diff --git a/sample/001_banner/configs/config.yaml b/sample/001_banner/configs/config.yaml new file mode 100755 index 00000000000..1dd053ddb22 --- /dev/null +++ b/sample/001_banner/configs/config.yaml @@ -0,0 +1,33 @@ +status-response: "ok" +adapters: + generic: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: /sample/file-settings.yaml + stored-requests-dir: /sample/stored + stored-imps-dir: /sample/stored + stored-responses-dir: /sample/stored + categories-dir: +gdpr: + default-value: 0 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false diff --git a/sample/001_banner/configs/file-settings.yaml b/sample/001_banner/configs/file-settings.yaml new file mode 100644 index 00000000000..c71ea718018 --- /dev/null +++ b/sample/001_banner/configs/file-settings.yaml @@ -0,0 +1,15 @@ +accounts: + - id: 1 + status: active + auction: + price-granularity: low + privacy: + ccpa: + enabled: false + gdpr: + enabled: false + cookie-sync: + default-limit: 8 + max-limit: 15 + coop-sync: + default: true diff --git a/sample/001_banner/data/pbjs.html b/sample/001_banner/data/pbjs.html new file mode 100644 index 00000000000..49ff94a6533 --- /dev/null +++ b/sample/001_banner/data/pbjs.html @@ -0,0 +1,122 @@ + + + + + + + + + + +

001_banner

+

+ This demo uses Prebid.js to interact with Prebid Server to fill the ad slot test-div-1 + The auction request to Prebid Server uses a stored request, which in turn links to a stored response.
+ Look for the /auction request in your browser's developer tool to inspect the request + and response. +

+

↓I am ad unit test-div-1 ↓

+
+
+ + + diff --git a/sample/001_banner/data/test-stored-request.json b/sample/001_banner/data/test-stored-request.json new file mode 100755 index 00000000000..cd4e69e854f --- /dev/null +++ b/sample/001_banner/data/test-stored-request.json @@ -0,0 +1,25 @@ +{ + "id": "test-stored-request", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "generic": {} + }, + "storedbidresponse": [ + { "bidder": "generic", "id": "test-stored-response" } + ] + } + } +} diff --git a/sample/001_banner/data/test-stored-response.json b/sample/001_banner/data/test-stored-response.json new file mode 100755 index 00000000000..6606e485a46 --- /dev/null +++ b/sample/001_banner/data/test-stored-response.json @@ -0,0 +1,46 @@ +{ + "id": "test-stored-response", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-div-1", + "price": 1, + "adm": "", + "adomain": [ + "www.addomain.com" + ], + "iurl": "http://localhost11", + "crid": "creative111", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "bidtype": 0, + "dspid": 6, + "origbidcpm": 1, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "generic" + }, + "targeting": { + "hb_bidder": "generic", + "hb_pb": "1.00", + "hb_size": "300x250" + }, + "type": "banner", + "video": { + "duration": 0, + "primary_category": "" + } + } + } + } + ], + "seat": "generic" + } + ], + "cur": "USD" +} diff --git a/sample/001_banner/docker-compose.yaml b/sample/001_banner/docker-compose.yaml new file mode 100644 index 00000000000..c6fb0f0aedd --- /dev/null +++ b/sample/001_banner/docker-compose.yaml @@ -0,0 +1,21 @@ +version: "3.9" +services: + 001_banner: + platform: linux/amd64 + build: + context: ../../ + dockerfile: Dockerfile + image: pbs-sample + container_name: 001_banner + privileged: true + environment: + JAVA_OPTS: "-Dspring.config.additional-location=/app/prebid-server/app.yaml" + ports: + - "8080:8080" + - "8060:8060" + volumes: + - ./configs/config.yaml:/app/prebid-server/app.yaml + - ./configs/file-settings.yaml:/sample/file-settings.yaml + - ./data/test-stored-request.json:/sample/stored/test-stored-request.json + - ./data/test-stored-response.json:/sample/stored/test-stored-response.json + - ./data/pbjs.html:/app/prebid-server/static/pbjs.html diff --git a/sample/prebid-config-db.yaml b/sample/configs/prebid-config-db.yaml similarity index 100% rename from sample/prebid-config-db.yaml rename to sample/configs/prebid-config-db.yaml diff --git a/sample/configs/prebid-config-s3.yaml b/sample/configs/prebid-config-s3.yaml new file mode 100644 index 00000000000..277ad94613c --- /dev/null +++ b/sample/configs/prebid-config-s3.yaml @@ -0,0 +1,60 @@ +status-response: "ok" + +server: + enable-quickack: true + enable-reuseport: true + +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + s3: + accessKeyId: prebid-server-test + secretAccessKey: nq9h6whXQURNL2NnWg3rcMlLMtGGDJeWrdl8hC9g + endpoint: http://localhost:9000 + bucket: prebid-server-configs.example.com # prebid-application-settings + force-path-style: true # virtual bucketing + # 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 + + in-memory-cache: + cache-size: 10000 + ttl-seconds: 1200 # 20 minutes + s3-update: + refresh-rate: 900000 # Refresh every 15 minutes + timeout: 5000 + +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 + +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false diff --git a/sample/prebid-config-with-module.yaml b/sample/configs/prebid-config-with-module.yaml similarity index 100% rename from sample/prebid-config-with-module.yaml rename to sample/configs/prebid-config-with-module.yaml diff --git a/sample/prebid-config.yaml b/sample/configs/prebid-config.yaml similarity index 79% rename from sample/prebid-config.yaml rename to sample/configs/prebid-config.yaml index dc5921bc41e..93a53b7952b 100644 --- a/sample/prebid-config.yaml +++ b/sample/configs/prebid-config.yaml @@ -21,10 +21,10 @@ settings: enforce-valid-account: false generate-storedrequest-bidrequest-id: true filesystem: - settings-filename: sample/sample-app-settings.yaml - stored-requests-dir: sample/stored - stored-imps-dir: sample/stored - stored-responses-dir: sample/stored + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample categories-dir: gdpr: default-value: 1 diff --git a/sample/sample-app-settings.yaml b/sample/configs/sample-app-settings.yaml similarity index 100% rename from sample/sample-app-settings.yaml rename to sample/configs/sample-app-settings.yaml diff --git a/sample/prebid-config-with-51d-dd.yaml b/sample/prebid-config-with-51d-dd.yaml new file mode 100644 index 00000000000..f32674538a3 --- /dev/null +++ b/sample/prebid-config-with-51d-dd.yaml @@ -0,0 +1,99 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/sample-app-settings.yaml + stored-requests-dir: sample/stored + stored-imps-dir: sample/stored + stored-responses-dir: sample/stored + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + 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" + } + ] + } + ] + } + } + } + } + } + modules: + fiftyone-devicedetection: + account-filter: + allow-list: [] # list of strings + 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 + 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/sample/requests/README.txt b/sample/requests/README.txt deleted file mode 100644 index 525400b9975..00000000000 --- a/sample/requests/README.txt +++ /dev/null @@ -1,12 +0,0 @@ -Each of these test requests works with prebid-config-file-bidders.yaml and the files in the samples/stored directory. - -You can invoke them with: - -curl --header "X-Forwarded-For: 151.101.194.216" -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Referer: https://example.com/demo/' -H "Content-Type: application/json" http://localhost:8080/openrtb2/auction --data @FILENAME - -- rubicon-storedresponse.json - this is a request that calls for a stored-auction-response. - -- appnexus-disabled-gdpr.json - this is a request that actually calls the appnexus endpoint after disabling GDPR by setting regs.ext.gdpr:0 - -- pbs-stored-req-test-video.json - this is a stored-request/response chain returning a VAST document - diff --git a/sample/requests/appnexus-disabled-gdpr.json b/sample/requests/appnexus-disabled-gdpr.json deleted file mode 100644 index 559d7e3f75a..00000000000 --- a/sample/requests/appnexus-disabled-gdpr.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "id": "7b3e93dc-10cc-42cd-b855-76ddea2e3f7d", - "source": { - "tid": "7b3e93dc-10cc-42cd-b855-76ddea2e3f7d" - }, - "tmax": 1000, - "imp": [ - { - "id": "test-div", - "ext": { - "appnexus": { - "placementId": 13144370 - } - }, - "secure": 0, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 728, - "h": 90 - } - ] - } - } - ], - "test": 1, - "site": { - "publisher": { - "id": "1001" - }, - "page": "http://rubitest.com/index.html" - }, - "ext": { - "prebid": { - "cache": { - "bids": {}, - "vastXml": {} - }, - "targeting": { - "pricegranularity": "med", - "includewinners": true, - "includebidderkeys": true - } - } - }, - "regs": { - "ext": { - "gdpr": 0 - } - } -} diff --git a/sample/requests/pbs-stored-req-resp-video.json b/sample/requests/pbs-stored-req-resp-video.json deleted file mode 100644 index 6f24704c9f4..00000000000 --- a/sample/requests/pbs-stored-req-resp-video.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "test": 1, - "tmax": 1000, - "id": "test-auction-id", - "imp": [ - { - "id": "a", - "video": { - }, - "ext": { - "prebid": { - "storedrequest": { - "id": "1001-sreq-test-prebid-vast" - } - } - } - } - ], - "site": { - "publisher": { - "id": "1001" - } - }, - "ext": { - "prebid": { - "targeting": { - "includewinners": true, - "includebidderkeys": true - }, - "storedrequest": { "id": "1001-sreq-test-top" } - } - } -} diff --git a/sample/requests/rubicon-storedresponse.json b/sample/requests/rubicon-storedresponse.json deleted file mode 100644 index d2773160658..00000000000 --- a/sample/requests/rubicon-storedresponse.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "id": "1", - "source": { - "tid": "7b3e93dc-10cc-42cd-b855-76ddea2e3f7d" - }, - "tmax": 1000, - "imp": [ - { - "id": "a", - "ext": { - "prebid": { - "storedauctionresponse": { "id": "1001-sar-320x50-imp-1" }, - "bidder": { - "rubicon": { - "accountId": 1001, - "siteId": 267318, - "zoneId": 1861698 - } - }}}, - "secure": 1, - "banner": { - "format": [ - { - "w": 320, - "h": 50 - } - ] - } - } - ], - "test": 1, - "site": { - "publisher": { - "id": "1001" - }, - "page": "http://example.com/prebid_server_test.html" - }, - "ext": { - "prebid": { - "targeting": { - "includewinners": true, - "includebidderkeys": true - } - } - } -} diff --git a/sample/stored/1001-sar-320x50-imp-1.json b/sample/stored/1001-sar-320x50-imp-1.json deleted file mode 100644 index 929ba7a2cbf..00000000000 --- a/sample/stored/1001-sar-320x50-imp-1.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "bid": [ - { - "h": 50, - "w": 320, - "id": "1", - "adm": "", - "ext": { - "prebid": { - "type": "banner" - } - }, - "crid": "888888", - "impid": "1", - "price": 1.23 - } - ], - "seat": "rubicon", - "group": 0 - } -] diff --git a/sample/stored/1001-sar-prebid-vast.json b/sample/stored/1001-sar-prebid-vast.json deleted file mode 100644 index 55ac62ad414..00000000000 --- a/sample/stored/1001-sar-prebid-vast.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "bid": [ - { - "h": 480, - "w": 640, - "id": "a121a07f-1579-4465-bc5e", - "adm": "Prebid TestVAST 2.0 Linear Ad00:00:15", - "ext": { - "prebid": { - "type": "video" - } - }, - "crid": "888888", - "impid": "##PBSIMPID##", - "price": 1.23 - } - ], - "seat": "rubicon", - "group": 0 - } -] diff --git a/sample/stored/1001-sreq-test-prebid-vast.json b/sample/stored/1001-sreq-test-prebid-vast.json deleted file mode 100644 index ffba54a2128..00000000000 --- a/sample/stored/1001-sreq-test-prebid-vast.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "ext": { - "prebid": { - "bidder": { - "rubicon": { - "siteId": 267318, - "zoneId": 1861698, - "accountId": 1001 - } - }, - "storedauctionresponse": { - "id": "1001-sar-prebid-vast" - } - } - }, - "video": { - "h": 480, - "w": 640, - "api": [ - 2 - ], - "mimes": [ - "video/mp4", - "video/x-ms-wmv" - ], - "context": "instream", - "linearity": 1, - "protocols": [ - 2, - 3, - 5, - 6 - ], - "playerSize": [ - [ - 640, - 480 - ] - ], - "maxduration": 30 - } -} diff --git a/sample/stored/1001-sreq-test-top.json b/sample/stored/1001-sreq-test-top.json deleted file mode 100644 index 0172925cc03..00000000000 --- a/sample/stored/1001-sreq-test-top.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "tmax": 2000, - "ext": { - "prebid": { - "cache": { - "bids": {} - }, - "targeting": { - "includewinners": true - } - } - } -} diff --git a/src/main/docker/run.sh b/src/main/docker/run.sh index 54f73437643..884aa8b3fd1 100755 --- a/src/main/docker/run.sh +++ b/src/main/docker/run.sh @@ -5,4 +5,4 @@ exec java \ -Dspring.config.additional-location=/app/prebid-server/,/app/prebid-server/conf/ \ ${JAVA_OPTS} \ -jar \ - /app/prebid-server/prebid-server.jar + /app/prebid-server/prebid-server.jar "$@" diff --git a/src/main/java/com/iab/openrtb/request/Eid.java b/src/main/java/com/iab/openrtb/request/Eid.java index f8a728e93cb..04892b57cc6 100644 --- a/src/main/java/com/iab/openrtb/request/Eid.java +++ b/src/main/java/com/iab/openrtb/request/Eid.java @@ -1,33 +1,24 @@ package com.iab.openrtb.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; import java.util.List; -/** - * Extended identifiers support in the OpenRTB specification allows buyers - * to use audience data in real-time bidding. This object can contain one - * or more {@link Uid}s from a single source or a technology provider. The - * exchange should ensure that business agreements allow for the sending - * of this data. - */ -@Value(staticConstructor = "of") +@Value +@Builder(toBuilder = true) public class Eid { - /** - * Source or technology provider responsible for the set of included IDs. Expressed as a top-level domain. - */ String source; - /** - * Array of extended ID {@link Uid} objects from the given source. - * Refer to 3.2.28 Extended Identifier UIDs - */ List uids; - /** - * Placeholder for vendor specific extensions to this object - */ + String inserter; + + String matcher; + + Integer mm; + ObjectNode ext; } diff --git a/src/main/java/com/iab/openrtb/request/Uid.java b/src/main/java/com/iab/openrtb/request/Uid.java index 536d5a1e02b..c90e563b9d5 100644 --- a/src/main/java/com/iab/openrtb/request/Uid.java +++ b/src/main/java/com/iab/openrtb/request/Uid.java @@ -1,6 +1,7 @@ package com.iab.openrtb.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; /** @@ -8,7 +9,8 @@ * extended identifiers. The exchange should ensure that business * agreements allow for the sending of this data. */ -@Value(staticConstructor = "of") +@Value +@Builder(toBuilder = true) public class Uid { /** diff --git a/src/main/java/com/iab/openrtb/request/Video.java b/src/main/java/com/iab/openrtb/request/Video.java index f967886bf89..369d576a3ac 100644 --- a/src/main/java/com/iab/openrtb/request/Video.java +++ b/src/main/java/com/iab/openrtb/request/Video.java @@ -254,6 +254,12 @@ public class Video { */ List companiontype; + /** + * Indicates pod deduplication settings that will be applied to bid responses. Refer to + * List: Pod Deduplication in AdCOM 1.0. + */ + List poddedupe; + /** * An array of objects (Section 3.2.35) * indicating the floor prices for video creatives of various durations that the buyer may bid with. diff --git a/src/main/java/org/prebid/server/activity/ActivitiesConfigResolver.java b/src/main/java/org/prebid/server/activity/ActivitiesConfigResolver.java new file mode 100644 index 00000000000..c9319d5c7cd --- /dev/null +++ b/src/main/java/org/prebid/server/activity/ActivitiesConfigResolver.java @@ -0,0 +1,102 @@ +package org.prebid.server.activity; + +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.activity.AccountActivityConfiguration; +import org.prebid.server.settings.model.activity.rule.AccountActivityConditionsRuleConfig; +import org.prebid.server.settings.model.activity.rule.AccountActivityRuleConfig; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ActivitiesConfigResolver { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(ActivitiesConfigResolver.class)); + + private final double logSamplingRate; + + public ActivitiesConfigResolver(double logSamplingRate) { + this.logSamplingRate = logSamplingRate; + } + + public Account resolve(Account account) { + if (!isInvalidActivitiesConfiguration(account)) { + return account; + } + + conditionalLogger.warn( + "Activity configuration for account %s contains conditional rule with empty array." + .formatted(account.getId()), + logSamplingRate); + + final AccountPrivacyConfig accountPrivacyConfig = account.getPrivacy(); + return account.toBuilder() + .privacy(accountPrivacyConfig.toBuilder() + .activities(removeInvalidRules(accountPrivacyConfig.getActivities())) + .build()) + .build(); + } + + private static boolean isInvalidActivitiesConfiguration(Account account) { + return Optional.ofNullable(account) + .map(Account::getPrivacy) + .map(AccountPrivacyConfig::getActivities) + .stream() + .map(Map::values) + .flatMap(Collection::stream) + .anyMatch(ActivitiesConfigResolver::containsInvalidRule); + } + + private static boolean containsInvalidRule(AccountActivityConfiguration accountActivityConfiguration) { + return Optional.ofNullable(accountActivityConfiguration) + .map(AccountActivityConfiguration::getRules) + .stream() + .flatMap(Collection::stream) + .anyMatch(ActivitiesConfigResolver::isInvalidConditionRule); + } + + private static boolean isInvalidConditionRule(AccountActivityRuleConfig rule) { + if (rule instanceof AccountActivityConditionsRuleConfig conditionsRule) { + final AccountActivityConditionsRuleConfig.Condition condition = conditionsRule.getCondition(); + return condition != null && isInvalidCondition(condition); + } + + return false; + } + + private static boolean isInvalidCondition(AccountActivityConditionsRuleConfig.Condition condition) { + return isEmptyNotNull(condition.getComponentTypes()) || isEmptyNotNull(condition.getComponentNames()); + } + + private static boolean isEmptyNotNull(Collection collection) { + return collection != null && collection.isEmpty(); + } + + private static Map removeInvalidRules( + Map activitiesConfiguration) { + + return activitiesConfiguration.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> removeInvalidRules(entry.getValue()))); + } + + private static AccountActivityConfiguration removeInvalidRules(AccountActivityConfiguration activityConfiguration) { + if (!containsInvalidRule(activityConfiguration)) { + return activityConfiguration; + } + + return AccountActivityConfiguration.of( + activityConfiguration.getAllow(), + activityConfiguration.getRules().stream() + .map(rule -> !isInvalidConditionRule(rule) ? rule : null) + .filter(Objects::nonNull) + .toList()); + } +} diff --git a/src/main/java/org/prebid/server/activity/Activity.java b/src/main/java/org/prebid/server/activity/Activity.java index ac4b87293bc..e56b37ce207 100644 --- a/src/main/java/org/prebid/server/activity/Activity.java +++ b/src/main/java/org/prebid/server/activity/Activity.java @@ -1,30 +1,39 @@ package org.prebid.server.activity; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; public enum Activity { @JsonProperty("syncUser") + @JsonAlias({"sync_user", "sync-user"}) SYNC_USER, @JsonProperty("fetchBids") + @JsonAlias({"fetch_bids", "fetch-bids"}) CALL_BIDDER, @JsonProperty("enrichUfpd") + @JsonAlias({"enrich_ufpd", "enrich-ufpd"}) MODIFY_UFDP, @JsonProperty("transmitUfpd") + @JsonAlias({"transmit_ufpd", "transmit-ufpd"}) TRANSMIT_UFPD, @JsonProperty("transmitEids") + @JsonAlias({"transmit_eids", "transmit-eids"}) TRANSMIT_EIDS, @JsonProperty("transmitPreciseGeo") + @JsonAlias({"transmit_precise_geo", "transmit-precise-geo"}) TRANSMIT_GEO, @JsonProperty("transmitTid") + @JsonAlias({"transmit_tid", "transmit-tid"}) TRANSMIT_TID, @JsonProperty("reportAnalytics") + @JsonAlias({"report_analytics", "report-analytics"}) REPORT_ANALYTICS } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/ActivityInfrastructure.java b/src/main/java/org/prebid/server/activity/infrastructure/ActivityInfrastructure.java index 220e46ab9ca..e4b6c54994c 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/ActivityInfrastructure.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/ActivityInfrastructure.java @@ -2,6 +2,7 @@ package org.prebid.server.activity.infrastructure; import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.debug.ActivityInfrastructureDebug; import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.proto.openrtb.ext.response.ExtTraceActivityInfrastructure; @@ -40,6 +41,10 @@ public boolean isAllowed(Activity activity, ActivityInvocationPayload activityIn return result; } + public void updateActivityMetrics(Activity activity, ComponentType componentType, String componentName) { + debug.updateActivityMetrics(activity, componentType, componentName); + } + public List debugTrace() { return debug.trace(); } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java index 9339582f1e1..49e24a06183 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java @@ -1,7 +1,5 @@ package org.prebid.server.activity.infrastructure.creator; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.ListUtils; import org.prebid.server.activity.Activity; import org.prebid.server.activity.infrastructure.ActivityController; @@ -11,6 +9,8 @@ import org.prebid.server.activity.infrastructure.rule.Rule; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.request.TraceLevel; @@ -124,7 +124,7 @@ private Function fallbackActivity( .or(() -> Optional.ofNullable(defaultPurpose4)) .map(Purpose::getEid) .map(PurposeEid::getActivityTransition) - .orElse(true); + .orElse(false); return originalActivity -> originalActivity == Activity.TRANSMIT_EIDS && imitateTransmitEids ? activityControllerCreator.apply(Activity.TRANSMIT_UFPD) diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java index 6d8a308fc1a..026e856a585 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreator.java @@ -55,7 +55,7 @@ public USCustomLogicModuleCreator(USCustomLogicGppReaderFactory gppReaderFactory this.metrics = Objects.requireNonNull(metrics); jsonLogicNodesCache = cacheTtl != null && cacheSize != null - ? SettingsCache.createCache(cacheTtl, cacheSize) + ? SettingsCache.createCache(cacheTtl, cacheSize, 0) : null; } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/ComponentRuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/ComponentRuleCreator.java deleted file mode 100644 index 7578a9536d1..00000000000 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/ComponentRuleCreator.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.prebid.server.activity.infrastructure.creator.rule; - -import org.prebid.server.activity.ComponentType; -import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.activity.infrastructure.creator.ActivityControllerCreationContext; -import org.prebid.server.activity.infrastructure.rule.ComponentRule; -import org.prebid.server.activity.infrastructure.rule.Rule; -import org.prebid.server.settings.model.activity.rule.AccountActivityComponentRuleConfig; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; -import java.util.TreeSet; - -public class ComponentRuleCreator extends AbstractRuleCreator { - - public ComponentRuleCreator() { - super(AccountActivityComponentRuleConfig.class); - } - - @Override - protected Rule fromConfiguration(AccountActivityComponentRuleConfig ruleConfiguration, - ActivityControllerCreationContext creationContext) { - - final boolean allow = allowFromConfig(ruleConfiguration.getAllow()); - final AccountActivityComponentRuleConfig.Condition condition = ruleConfiguration.getCondition(); - - return new ComponentRule( - condition != null ? setOf(condition.getComponentTypes()) : null, - condition != null ? caseInsensitiveSetOf(condition.getComponentNames()) : null, - allow); - } - - private static boolean allowFromConfig(Boolean configValue) { - return configValue != null ? configValue : ActivityInfrastructure.ALLOW_ACTIVITY_BY_DEFAULT; - } - - private static Set setOf(Collection collection) { - return collection != null ? new HashSet<>(collection) : null; - } - - private static Set caseInsensitiveSetOf(Collection collection) { - if (collection == null) { - return null; - } - - final Set caseInsensitiveSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - caseInsensitiveSet.addAll(collection); - return caseInsensitiveSet; - } -} diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreator.java new file mode 100644 index 00000000000..593fbc69b2c --- /dev/null +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreator.java @@ -0,0 +1,94 @@ +package org.prebid.server.activity.infrastructure.creator.rule; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.activity.infrastructure.creator.ActivityControllerCreationContext; +import org.prebid.server.activity.infrastructure.rule.ConditionsRule; +import org.prebid.server.activity.infrastructure.rule.Rule; +import org.prebid.server.settings.model.activity.rule.AccountActivityConditionsRuleConfig; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +public class ConditionsRuleCreator extends AbstractRuleCreator { + + public ConditionsRuleCreator() { + super(AccountActivityConditionsRuleConfig.class); + } + + @Override + protected Rule fromConfiguration(AccountActivityConditionsRuleConfig ruleConfiguration, + ActivityControllerCreationContext creationContext) { + + final boolean allow = allowFromConfig(ruleConfiguration.getAllow()); + final AccountActivityConditionsRuleConfig.Condition condition = ruleConfiguration.getCondition(); + + return new ConditionsRule( + condition != null ? setOf(condition.getComponentTypes()) : null, + condition != null ? caseInsensitiveSetOf(condition.getComponentNames()) : null, + sidsMatched(condition, creationContext.getGppContext().scope().getSectionsIds()), + condition != null ? geoCodes(condition.getGeoCodes()) : null, + condition != null ? condition.getGpc() : null, + allow); + } + + private static boolean allowFromConfig(Boolean configValue) { + return configValue != null ? configValue : ActivityInfrastructure.ALLOW_ACTIVITY_BY_DEFAULT; + } + + private static Set setOf(Collection collection) { + return collection != null ? new HashSet<>(collection) : null; + } + + private static Set caseInsensitiveSetOf(Collection collection) { + if (collection == null) { + return null; + } + + final Set caseInsensitiveSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + caseInsensitiveSet.addAll(collection); + return caseInsensitiveSet; + } + + private static boolean sidsMatched(AccountActivityConditionsRuleConfig.Condition condition, Set gppSids) { + final List sids = condition != null ? condition.getSids() : null; + return sids == null || intersects(sids, gppSids); + } + + private static boolean intersects(Collection configurationSids, Collection gppSids) { + return CollectionUtils.isNotEmpty(configurationSids) && CollectionUtils.isNotEmpty(gppSids) + && !CollectionUtils.intersection(configurationSids, gppSids).isEmpty(); + } + + private static List geoCodes(List stringGeoCodes) { + return stringGeoCodes != null + ? stringGeoCodes.stream() + .map(ConditionsRuleCreator::from) + .filter(Objects::nonNull) + .toList() + : null; + } + + private static ConditionsRule.GeoCode from(String stringGeoCode) { + if (StringUtils.isBlank(stringGeoCode)) { + return null; + } + + final int firstDot = stringGeoCode.indexOf("."); + if (firstDot == -1) { + return ConditionsRule.GeoCode.of(stringGeoCode, null); + } else if (firstDot == stringGeoCode.length() - 1) { + return ConditionsRule.GeoCode.of(stringGeoCode.substring(0, firstDot), null); + } + + return ConditionsRule.GeoCode.of( + stringGeoCode.substring(0, firstDot), + stringGeoCode.substring(firstDot + 1)); + } +} diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreator.java deleted file mode 100644 index 96347390575..00000000000 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/GeoRuleCreator.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.prebid.server.activity.infrastructure.creator.rule; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.activity.ComponentType; -import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.activity.infrastructure.creator.ActivityControllerCreationContext; -import org.prebid.server.activity.infrastructure.rule.GeoRule; -import org.prebid.server.activity.infrastructure.rule.Rule; -import org.prebid.server.settings.model.activity.rule.AccountActivityGeoRuleConfig; - -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; - -public class GeoRuleCreator extends AbstractRuleCreator { - - public GeoRuleCreator() { - super(AccountActivityGeoRuleConfig.class); - } - - @Override - protected Rule fromConfiguration(AccountActivityGeoRuleConfig ruleConfiguration, - ActivityControllerCreationContext creationContext) { - - final boolean allow = allowFromConfig(ruleConfiguration.getAllow()); - final AccountActivityGeoRuleConfig.Condition condition = ruleConfiguration.getCondition(); - - return new GeoRule( - condition != null ? setOf(condition.getComponentTypes()) : null, - condition != null ? caseInsensitiveSetOf(condition.getComponentNames()) : null, - sidsMatched(condition, creationContext.getGppContext().scope().getSectionsIds()), - condition != null ? geoCodes(condition.getGeoCodes()) : null, - condition != null ? condition.getGpc() : null, - allow); - } - - private static boolean allowFromConfig(Boolean configValue) { - return configValue != null ? configValue : ActivityInfrastructure.ALLOW_ACTIVITY_BY_DEFAULT; - } - - private static Set setOf(Collection collection) { - return collection != null ? new HashSet<>(collection) : null; - } - - private static Set caseInsensitiveSetOf(Collection collection) { - if (collection == null) { - return null; - } - - final Set caseInsensitiveSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - caseInsensitiveSet.addAll(collection); - return caseInsensitiveSet; - } - - private static boolean sidsMatched(AccountActivityGeoRuleConfig.Condition condition, Set gppSids) { - final List sids = condition != null ? condition.getSids() : null; - return sids == null || intersects(sids, gppSids); - } - - private static boolean intersects(Collection configurationSids, Collection gppSids) { - return CollectionUtils.isNotEmpty(configurationSids) && CollectionUtils.isNotEmpty(gppSids) - && !CollectionUtils.intersection(configurationSids, gppSids).isEmpty(); - } - - private static List geoCodes(List stringGeoCodes) { - return stringGeoCodes != null - ? stringGeoCodes.stream() - .map(GeoRuleCreator::from) - .filter(Objects::nonNull) - .toList() - : null; - } - - private static GeoRule.GeoCode from(String stringGeoCode) { - if (StringUtils.isBlank(stringGeoCode)) { - return null; - } - - final int firstDot = stringGeoCode.indexOf("."); - if (firstDot == -1) { - return GeoRule.GeoCode.of(stringGeoCode, null); - } else if (firstDot == stringGeoCode.length() - 1) { - return GeoRule.GeoCode.of(stringGeoCode.substring(0, firstDot), null); - } - - return GeoRule.GeoCode.of( - stringGeoCode.substring(0, firstDot), - stringGeoCode.substring(firstDot + 1)); - } -} diff --git a/src/main/java/org/prebid/server/activity/infrastructure/debug/ActivityInfrastructureDebug.java b/src/main/java/org/prebid/server/activity/infrastructure/debug/ActivityInfrastructureDebug.java index 2d1c76f3fa0..9d15023d559 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/debug/ActivityInfrastructureDebug.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/debug/ActivityInfrastructureDebug.java @@ -81,13 +81,20 @@ public void emitActivityInvocationResult(Activity activity, } if (!result) { - metrics.updateRequestsActivityDisallowedCount(activity); - if (atLeast(TraceLevel.verbose)) { - metrics.updateAccountActivityDisallowedCount(accountId, activity); - } - if (activityInvocationPayload.componentType() == ComponentType.BIDDER) { - metrics.updateAdapterActivityDisallowedCount(activityInvocationPayload.componentName(), activity); - } + updateActivityMetrics( + activity, + activityInvocationPayload.componentType(), + activityInvocationPayload.componentName()); + } + } + + public void updateActivityMetrics(Activity activity, ComponentType componentType, String componentName) { + metrics.updateRequestsActivityDisallowedCount(activity); + if (atLeast(TraceLevel.verbose)) { + metrics.updateAccountActivityDisallowedCount(accountId, activity); + } + if (componentType == ComponentType.BIDDER) { + metrics.updateAdapterActivityDisallowedCount(componentName, activity); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/PrivacySection.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/PrivacySection.java index a35982c532d..3e081a6310e 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/PrivacySection.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/PrivacySection.java @@ -1,22 +1,22 @@ package org.prebid.server.activity.infrastructure.privacy; -import com.iab.gpp.encoder.section.UspCaV1; -import com.iab.gpp.encoder.section.UspCoV1; -import com.iab.gpp.encoder.section.UspCtV1; -import com.iab.gpp.encoder.section.UspNatV1; -import com.iab.gpp.encoder.section.UspUtV1; -import com.iab.gpp.encoder.section.UspVaV1; +import com.iab.gpp.encoder.section.UsCaV1; +import com.iab.gpp.encoder.section.UsCoV1; +import com.iab.gpp.encoder.section.UsCtV1; +import com.iab.gpp.encoder.section.UsNatV1; +import com.iab.gpp.encoder.section.UsUtV1; +import com.iab.gpp.encoder.section.UsVaV1; import java.util.Set; public enum PrivacySection { - NATIONAL(UspNatV1.ID), - CALIFORNIA(UspCaV1.ID), - VIRGINIA(UspVaV1.ID), - COLORADO(UspCoV1.ID), - UTAH(UspUtV1.ID), - CONNECTICUT(UspCtV1.ID); + NATIONAL(UsNatV1.ID), + CALIFORNIA(UsCaV1.ID), + VIRGINIA(UsVaV1.ID), + COLORADO(UsCoV1.ID), + UTAH(UsUtV1.ID), + CONNECTICUT(UsCtV1.ID); public static final Set US_PRIVACY_SECTIONS = Set.of( NATIONAL, diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USCaliforniaGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USCaliforniaGppReader.java index 5ac3e9601a5..963f50ab2b1 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USCaliforniaGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USCaliforniaGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.uscustomlogic.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspCaV1; +import com.iab.gpp.encoder.section.UsCaV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.util.ObjectUtil; @@ -9,20 +9,20 @@ public class USCaliforniaGppReader implements USCustomLogicGppReader { - private final UspCaV1 consent; + private final UsCaV1 consent; public USCaliforniaGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCaV1Section() : null; + consent = gppModel != null ? gppModel.getUsCaV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getGpc); } @Override @@ -32,17 +32,17 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSaleOptOutNotice); } @Override @@ -52,12 +52,12 @@ public Integer getSharingNotice() { @Override public Integer getSharingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSharingOptOut); } @Override public Integer getSharingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSharingOptOutNotice); } @Override @@ -72,12 +72,12 @@ public Integer getTargetedAdvertisingOptOutNotice() { @Override public Integer getSensitiveDataLimitUseNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSensitiveDataLimitUseNotice); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSensitiveDataLimitUseNotice); } @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSensitiveDataProcessing); } @Override @@ -87,26 +87,26 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public List getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getKnownChildSensitiveDataConsents); } @Override public Integer getPersonalDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getPersonalDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getPersonalDataConsents); } @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USColoradoGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USColoradoGppReader.java index 8a6d1c457e4..bdc3889f6e0 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USColoradoGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USColoradoGppReader.java @@ -2,7 +2,7 @@ package org.prebid.server.activity.infrastructure.privacy.uscustomlogic.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspCoV1; +import com.iab.gpp.encoder.section.UsCoV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.util.ObjectUtil; @@ -10,20 +10,20 @@ public class USColoradoGppReader implements USCustomLogicGppReader { - private final UspCoV1 consent; + private final UsCoV1 consent; public USColoradoGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCoV1Section() : null; + consent = gppModel != null ? gppModel.getUsCoV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getGpc); } @Override @@ -33,22 +33,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSharingNotice); } @Override @@ -63,12 +63,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -78,7 +78,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSensitiveDataProcessing); } @Override @@ -88,7 +88,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public Integer getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getKnownChildSensitiveDataConsents); } @Override @@ -98,16 +98,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USConnecticutGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USConnecticutGppReader.java index 9c617577099..d7d588926f8 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USConnecticutGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USConnecticutGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.uscustomlogic.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspCtV1; +import com.iab.gpp.encoder.section.UsCtV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.util.ObjectUtil; @@ -9,20 +9,20 @@ public class USConnecticutGppReader implements USCustomLogicGppReader { - private final UspCtV1 consent; + private final UsCtV1 consent; public USConnecticutGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCtV1Section() : null; + consent = gppModel != null ? gppModel.getUsCtV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getGpc); } @Override @@ -32,22 +32,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSharingNotice); } @Override @@ -62,12 +62,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -77,7 +77,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSensitiveDataProcessing); } @Override @@ -87,7 +87,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public List getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getKnownChildSensitiveDataConsents); } @Override @@ -97,16 +97,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USUtahGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USUtahGppReader.java index d9c768c7dc3..3f34d024b35 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USUtahGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USUtahGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.uscustomlogic.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspUtV1; +import com.iab.gpp.encoder.section.UsUtV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.util.ObjectUtil; @@ -9,15 +9,15 @@ public class USUtahGppReader implements USCustomLogicGppReader { - private final UspUtV1 consent; + private final UsUtV1 consent; public USUtahGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspUtV1Section() : null; + consent = gppModel != null ? gppModel.getUsUtV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getVersion); } @Override @@ -37,17 +37,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSharingNotice); } @Override @@ -62,12 +62,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -77,17 +77,17 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSensitiveDataProcessing); } @Override public Integer getSensitiveDataProcessingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSensitiveDataProcessingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSensitiveDataProcessingOptOutNotice); } @Override public Integer getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getKnownChildSensitiveDataConsents); } @Override @@ -97,16 +97,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USVirginiaGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USVirginiaGppReader.java index a07986e4160..cccb0186946 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USVirginiaGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/uscustomlogic/reader/USVirginiaGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.uscustomlogic.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspVaV1; +import com.iab.gpp.encoder.section.UsVaV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.util.ObjectUtil; @@ -9,15 +9,15 @@ public class USVirginiaGppReader implements USCustomLogicGppReader { - private final UspVaV1 consent; + private final UsVaV1 consent; public USVirginiaGppReader(GppModel gppModel) { - this.consent = gppModel != null ? gppModel.getUspVaV1Section() : null; + this.consent = gppModel != null ? gppModel.getUsVaV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getVersion); } @Override @@ -37,17 +37,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSharingNotice); } @Override @@ -62,12 +62,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -77,7 +77,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSensitiveDataProcessing); } @Override @@ -87,7 +87,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public Integer getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getKnownChildSensitiveDataConsents); } @Override @@ -97,16 +97,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedCaliforniaGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedCaliforniaGppReader.java index 637685b774e..81878e5d908 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedCaliforniaGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedCaliforniaGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.usnat.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspCaV1; +import com.iab.gpp.encoder.section.UsCaV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatGppReader; import org.prebid.server.util.ObjectUtil; @@ -15,20 +15,20 @@ public class USMappedCaliforniaGppReader implements USNatGppReader, USCustomLogi private static final List DEFAULT_SENSITIVE_DATA = Collections.nCopies(12, null); private static final List CHILD_SENSITIVE_DATA = List.of(1, 1); - private final UspCaV1 consent; + private final UsCaV1 consent; public USMappedCaliforniaGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCaV1Section() : null; + consent = gppModel != null ? gppModel.getUsCaV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getGpc); } @Override @@ -38,17 +38,17 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSaleOptOutNotice); } @Override @@ -58,12 +58,12 @@ public Integer getSharingNotice() { @Override public Integer getSharingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSharingOptOut); } @Override public Integer getSharingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSharingOptOutNotice); } @Override @@ -78,7 +78,7 @@ public Integer getTargetedAdvertisingOptOutNotice() { @Override public Integer getSensitiveDataLimitUseNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSensitiveDataLimitUseNotice); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getSensitiveDataLimitUseNotice); } @Override @@ -117,21 +117,21 @@ public List getKnownChildSensitiveDataConsents() { @Override public Integer getPersonalDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getPersonalDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getPersonalDataConsents); } @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCaV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedColoradoGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedColoradoGppReader.java index 6ea4eefce91..b9505a7c458 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedColoradoGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedColoradoGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.usnat.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspCoV1; +import com.iab.gpp.encoder.section.UsCoV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatGppReader; import org.prebid.server.util.ObjectUtil; @@ -13,20 +13,20 @@ public class USMappedColoradoGppReader implements USNatGppReader, USCustomLogicG private static final List CHILD_SENSITIVE_DATA = List.of(1, 1); private static final List NON_CHILD_SENSITIVE_DATA = List.of(0, 0); - private final UspCoV1 consent; + private final UsCoV1 consent; public USMappedColoradoGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCoV1Section() : null; + consent = gppModel != null ? gppModel.getUsCoV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getGpc); } @Override @@ -36,22 +36,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSharingNotice); } @Override @@ -66,12 +66,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -81,7 +81,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getSensitiveDataProcessing); } @Override @@ -108,16 +108,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCoV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedConnecticutGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedConnecticutGppReader.java index 10a24f3f26f..8309e75ebc1 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedConnecticutGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedConnecticutGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.usnat.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspCtV1; +import com.iab.gpp.encoder.section.UsCtV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatGppReader; import org.prebid.server.util.ObjectUtil; @@ -15,20 +15,20 @@ public class USMappedConnecticutGppReader implements USNatGppReader, USCustomLog private static final List MIXED_CHILD_SENSITIVE_DATA = List.of(2, 1); private static final List CHILD_SENSITIVE_DATA = List.of(1, 1); - private final UspCtV1 consent; + private final UsCtV1 consent; public USMappedConnecticutGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCtV1Section() : null; + consent = gppModel != null ? gppModel.getUsCtV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getGpc); } @Override @@ -38,22 +38,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSharingNotice); } @Override @@ -68,12 +68,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -83,7 +83,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getSensitiveDataProcessing); } @Override @@ -94,7 +94,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public List getKnownChildSensitiveDataConsents() { final List originalData = ObjectUtil.getIfNotNull( - consent, UspCtV1::getKnownChildSensitiveDataConsents); + consent, UsCtV1::getKnownChildSensitiveDataConsents); final Integer first = originalData != null ? originalData.get(0) : null; final Integer second = originalData != null ? originalData.get(1) : null; @@ -116,16 +116,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCtV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedUtahGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedUtahGppReader.java index d808c96784f..527a87288f7 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedUtahGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedUtahGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.usnat.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspUtV1; +import com.iab.gpp.encoder.section.UsUtV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatGppReader; import org.prebid.server.util.ObjectUtil; @@ -17,15 +17,15 @@ public class USMappedUtahGppReader implements USNatGppReader, USCustomLogicGppRe private static final List CHILD_SENSITIVE_DATA = List.of(1, 1); private static final List NON_CHILD_SENSITIVE_DATA = List.of(0, 0); - private final UspUtV1 consent; + private final UsUtV1 consent; public USMappedUtahGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspUtV1Section() : null; + consent = gppModel != null ? gppModel.getUsUtV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getVersion); } @Override @@ -45,17 +45,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSharingNotice); } @Override @@ -70,12 +70,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -86,7 +86,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { final List originalData = Optional.ofNullable(consent) - .map(UspUtV1::getSensitiveDataProcessing) + .map(UsUtV1::getSensitiveDataProcessing) .orElse(DEFAULT_SENSITIVE_DATA_PROCESSING); final List data = new ArrayList<>(DEFAULT_SENSITIVE_DATA_PROCESSING); @@ -104,7 +104,7 @@ public List getSensitiveDataProcessing() { @Override public Integer getSensitiveDataProcessingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSensitiveDataProcessingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getSensitiveDataProcessingOptOutNotice); } @Override @@ -126,16 +126,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsUtV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedVirginiaGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedVirginiaGppReader.java index 97abfef6331..739762666ea 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedVirginiaGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USMappedVirginiaGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.usnat.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspVaV1; +import com.iab.gpp.encoder.section.UsVaV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatGppReader; import org.prebid.server.util.ObjectUtil; @@ -13,15 +13,15 @@ public class USMappedVirginiaGppReader implements USNatGppReader, USCustomLogicG private static final List CHILD_SENSITIVE_DATA = List.of(1, 1); private static final List NON_CHILD_SENSITIVE_DATA = List.of(0, 0); - private final UspVaV1 consent; + private final UsVaV1 consent; public USMappedVirginiaGppReader(GppModel gppModel) { - this.consent = gppModel != null ? gppModel.getUspVaV1Section() : null; + this.consent = gppModel != null ? gppModel.getUsVaV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getVersion); } @Override @@ -41,17 +41,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSharingNotice); } @Override @@ -66,12 +66,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getTargetedAdvertisingOptOutNotice); } @Override @@ -81,7 +81,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getSensitiveDataProcessing); } @Override @@ -108,16 +108,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsVaV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USNationalGppReader.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USNationalGppReader.java index 8e5332fd979..11dec8ebbc2 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USNationalGppReader.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/reader/USNationalGppReader.java @@ -1,7 +1,7 @@ package org.prebid.server.activity.infrastructure.privacy.usnat.reader; import com.iab.gpp.encoder.GppModel; -import com.iab.gpp.encoder.section.UspNatV1; +import com.iab.gpp.encoder.section.UsNatV1; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicGppReader; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatGppReader; import org.prebid.server.util.ObjectUtil; @@ -10,20 +10,20 @@ public class USNationalGppReader implements USNatGppReader, USCustomLogicGppReader { - private final UspNatV1 consent; + private final UsNatV1 consent; public USNationalGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspNatV1Section() : null; + consent = gppModel != null ? gppModel.getUsNatV1Section() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getGpc); } @Override @@ -37,81 +37,81 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSharingNotice); } @Override public Integer getSharingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSharingOptOut); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSharingOptOut); } @Override public Integer getSharingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSharingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSharingOptOutNotice); } @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getTargetedAdvertisingOptOutNotice); } @Override public Integer getSensitiveDataLimitUseNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSensitiveDataLimitUseNotice); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSensitiveDataLimitUseNotice); } @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSensitiveDataProcessing); } @Override public Integer getSensitiveDataProcessingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSensitiveDataProcessingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getSensitiveDataProcessingOptOutNotice); } @Override public List getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getKnownChildSensitiveDataConsents); } @Override public Integer getPersonalDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getPersonalDataConsents); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getPersonalDataConsents); } @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsNatV1::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/rule/ComponentRule.java b/src/main/java/org/prebid/server/activity/infrastructure/rule/ComponentRule.java deleted file mode 100644 index b45cbfa887c..00000000000 --- a/src/main/java/org/prebid/server/activity/infrastructure/rule/ComponentRule.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.prebid.server.activity.infrastructure.rule; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.prebid.server.activity.ComponentType; -import org.prebid.server.activity.infrastructure.debug.Loggable; -import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; - -import java.util.Set; - -public final class ComponentRule extends AbstractMatchRule implements Loggable { - - private final Set componentTypes; - private final Set componentNames; - private final boolean allowed; - - public ComponentRule(Set componentTypes, - Set componentNames, - boolean allowed) { - - this.componentTypes = componentTypes; - this.componentNames = componentNames; - this.allowed = allowed; - } - - @Override - public boolean matches(ActivityInvocationPayload activityInvocationPayload) { - return (componentTypes == null || componentTypes.contains(activityInvocationPayload.componentType())) - && (componentNames == null || componentNames.contains(activityInvocationPayload.componentName())); - } - - @Override - public boolean allowed() { - return allowed; - } - - @Override - public JsonNode asLogEntry(ObjectMapper mapper) { - return mapper.valueToTree(new ComponentRuleLogEntry(componentTypes, componentNames, allowed)); - } - - private record ComponentRuleLogEntry(Set componentTypes, - Set componentNames, - boolean allow) { - } -} diff --git a/src/main/java/org/prebid/server/activity/infrastructure/rule/ConditionsRule.java b/src/main/java/org/prebid/server/activity/infrastructure/rule/ConditionsRule.java new file mode 100644 index 00000000000..3bd4c21045d --- /dev/null +++ b/src/main/java/org/prebid/server/activity/infrastructure/rule/ConditionsRule.java @@ -0,0 +1,102 @@ +package org.prebid.server.activity.infrastructure.rule; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Value; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.debug.Loggable; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.payload.GeoActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.payload.GpcActivityInvocationPayload; + +import java.util.List; +import java.util.Set; + +public final class ConditionsRule extends AbstractMatchRule implements Loggable { + + private final Set componentTypes; + private final Set componentNames; + private final boolean sidsMatched; + private final List geoCodes; + private final String gpc; + private final boolean allowed; + + public ConditionsRule(Set componentTypes, + Set componentNames, + boolean sidsMatched, + List geoCodes, + String gpc, + boolean allowed) { + + this.componentTypes = componentTypes; + this.componentNames = componentNames; + this.sidsMatched = sidsMatched; + this.geoCodes = geoCodes; + this.gpc = gpc; + this.allowed = allowed; + } + + @Override + public boolean matches(ActivityInvocationPayload activityInvocationPayload) { + return sidsMatched + && (geoCodes == null || matchesOneOfGeoCodes(activityInvocationPayload)) + && (gpc == null || matchesGpc(activityInvocationPayload)) + && (componentTypes == null || componentTypes.contains(activityInvocationPayload.componentType())) + && (componentNames == null || componentNames.contains(activityInvocationPayload.componentName())); + } + + private boolean matchesOneOfGeoCodes(ActivityInvocationPayload activityInvocationPayload) { + if (activityInvocationPayload instanceof GeoActivityInvocationPayload geoPayload) { + return geoCodes.stream().anyMatch(geoCode -> matchesGeoCode(geoCode, geoPayload)); + } + + return true; + } + + private static boolean matchesGeoCode(GeoCode geoCode, GeoActivityInvocationPayload geoPayload) { + final String region = geoCode.getRegion(); + return StringUtils.equalsIgnoreCase(geoCode.getCountry(), geoPayload.country()) + && (region == null || StringUtils.equalsIgnoreCase(region, geoPayload.region())); + } + + private boolean matchesGpc(ActivityInvocationPayload activityInvocationPayload) { + if (activityInvocationPayload instanceof GpcActivityInvocationPayload gpcActivityInvocationPayload) { + return gpc.equals(gpcActivityInvocationPayload.gpc()); + } + + return true; + } + + @Override + public boolean allowed() { + return allowed; + } + + @Override + public JsonNode asLogEntry(ObjectMapper mapper) { + return mapper.valueToTree(new GeoRuleLogEntry( + componentTypes, + componentNames, + sidsMatched, + geoCodes, + gpc, + allowed)); + } + + @Value(staticConstructor = "of") + public static class GeoCode { + + String country; + + String region; + } + + private record GeoRuleLogEntry(Set componentTypes, + Set componentNames, + boolean gppSidsMatched, + List geoCodes, + String gpc, + boolean allow) { + } +} diff --git a/src/main/java/org/prebid/server/activity/infrastructure/rule/GeoRule.java b/src/main/java/org/prebid/server/activity/infrastructure/rule/GeoRule.java deleted file mode 100644 index fd08072b00b..00000000000 --- a/src/main/java/org/prebid/server/activity/infrastructure/rule/GeoRule.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.prebid.server.activity.infrastructure.rule; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Value; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.activity.ComponentType; -import org.prebid.server.activity.infrastructure.debug.Loggable; -import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; -import org.prebid.server.activity.infrastructure.payload.GeoActivityInvocationPayload; -import org.prebid.server.activity.infrastructure.payload.GpcActivityInvocationPayload; - -import java.util.List; -import java.util.Set; - -public final class GeoRule extends AbstractMatchRule implements Loggable { - - private final Set componentTypes; - private final Set componentNames; - private final boolean sidsMatched; - private final List geoCodes; - private final String gpc; - private final boolean allowed; - - public GeoRule(Set componentTypes, - Set componentNames, - boolean sidsMatched, - List geoCodes, - String gpc, - boolean allowed) { - - this.componentTypes = componentTypes; - this.componentNames = componentNames; - this.sidsMatched = sidsMatched; - this.geoCodes = geoCodes; - this.gpc = gpc; - this.allowed = allowed; - } - - @Override - public boolean matches(ActivityInvocationPayload activityInvocationPayload) { - return sidsMatched - && (geoCodes == null || matchesOneOfGeoCodes(activityInvocationPayload)) - && (gpc == null || matchesGpc(activityInvocationPayload)) - && (componentTypes == null || componentTypes.contains(activityInvocationPayload.componentType())) - && (componentNames == null || componentNames.contains(activityInvocationPayload.componentName())); - } - - private boolean matchesOneOfGeoCodes(ActivityInvocationPayload activityInvocationPayload) { - if (activityInvocationPayload instanceof GeoActivityInvocationPayload geoPayload) { - return geoCodes.stream().anyMatch(geoCode -> matchesGeoCode(geoCode, geoPayload)); - } - - return true; - } - - private static boolean matchesGeoCode(GeoCode geoCode, GeoActivityInvocationPayload geoPayload) { - final String region = geoCode.getRegion(); - return StringUtils.equalsIgnoreCase(geoCode.getCountry(), geoPayload.country()) - && (region == null || StringUtils.equalsIgnoreCase(region, geoPayload.region())); - } - - private boolean matchesGpc(ActivityInvocationPayload activityInvocationPayload) { - if (activityInvocationPayload instanceof GpcActivityInvocationPayload gpcActivityInvocationPayload) { - return gpc.equals(gpcActivityInvocationPayload.gpc()); - } - - return true; - } - - @Override - public boolean allowed() { - return allowed; - } - - @Override - public JsonNode asLogEntry(ObjectMapper mapper) { - return mapper.valueToTree(new GeoRuleLogEntry( - componentTypes, - componentNames, - sidsMatched, - geoCodes, - gpc, - allowed)); - } - - @Value(staticConstructor = "of") - public static class GeoCode { - - String country; - - String region; - } - - private record GeoRuleLogEntry(Set componentTypes, - Set componentNames, - boolean gppSidsMatched, - List geoCodes, - String gpc, - boolean allow) { - } -} diff --git a/src/main/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtils.java b/src/main/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtils.java deleted file mode 100644 index eccc75dee0f..00000000000 --- a/src/main/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtils.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.prebid.server.activity.utils; - -import org.prebid.server.activity.Activity; -import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountPrivacyConfig; -import org.prebid.server.settings.model.activity.AccountActivityConfiguration; -import org.prebid.server.settings.model.activity.rule.AccountActivityComponentRuleConfig; -import org.prebid.server.settings.model.activity.rule.AccountActivityGeoRuleConfig; -import org.prebid.server.settings.model.activity.rule.AccountActivityRuleConfig; - -import java.util.Collection; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -public class AccountActivitiesConfigurationUtils { - - private AccountActivitiesConfigurationUtils() { - } - - public static boolean isInvalidActivitiesConfiguration(Account account) { - return Optional.ofNullable(account) - .map(Account::getPrivacy) - .map(AccountPrivacyConfig::getActivities) - .stream() - .map(Map::values) - .flatMap(Collection::stream) - .anyMatch(AccountActivitiesConfigurationUtils::containsInvalidRule); - } - - private static boolean containsInvalidRule(AccountActivityConfiguration accountActivityConfiguration) { - return Optional.ofNullable(accountActivityConfiguration) - .map(AccountActivityConfiguration::getRules) - .stream() - .flatMap(Collection::stream) - .anyMatch(AccountActivitiesConfigurationUtils::isInvalidConditionRule); - } - - private static boolean isInvalidConditionRule(AccountActivityRuleConfig rule) { - if (rule instanceof AccountActivityComponentRuleConfig conditionRule) { - final AccountActivityComponentRuleConfig.Condition condition = conditionRule.getCondition(); - return condition != null && isInvalidCondition(condition); - } - - if (rule instanceof AccountActivityGeoRuleConfig geoRule) { - final AccountActivityGeoRuleConfig.Condition condition = geoRule.getCondition(); - return condition != null && isInvalidCondition(condition); - } - - return false; - } - - private static boolean isInvalidCondition(AccountActivityComponentRuleConfig.Condition condition) { - return isEmptyNotNull(condition.getComponentTypes()) || isEmptyNotNull(condition.getComponentNames()); - } - - private static boolean isInvalidCondition(AccountActivityGeoRuleConfig.Condition condition) { - return isEmptyNotNull(condition.getComponentTypes()) || isEmptyNotNull(condition.getComponentNames()); - } - - private static boolean isEmptyNotNull(Collection collection) { - return collection != null && collection.isEmpty(); - } - - public static Map removeInvalidRules( - Map activitiesConfiguration) { - - return activitiesConfiguration.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> AccountActivitiesConfigurationUtils.removeInvalidRules(entry.getValue()))); - } - - private static AccountActivityConfiguration removeInvalidRules(AccountActivityConfiguration activityConfiguration) { - if (!containsInvalidRule(activityConfiguration)) { - return activityConfiguration; - } - - return AccountActivityConfiguration.of( - activityConfiguration.getAllow(), - activityConfiguration.getRules().stream() - .map(rule -> !isInvalidConditionRule(rule) ? rule : null) - .filter(Objects::nonNull) - .toList()); - } -} diff --git a/src/main/java/org/prebid/server/analytics/model/AmpEvent.java b/src/main/java/org/prebid/server/analytics/model/AmpEvent.java index cf33545a332..fc3e96c913c 100644 --- a/src/main/java/org/prebid/server/analytics/model/AmpEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/AmpEvent.java @@ -13,7 +13,7 @@ /** * Represents a transaction at /openrtb2/amp endpoint. */ -@Builder +@Builder(toBuilder = true) @Value public class AmpEvent { diff --git a/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java b/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java index 090dd818f07..9bbfdc88845 100644 --- a/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java @@ -20,8 +20,6 @@ public class NotificationEvent { Account account; - String lineItemId; - String bidder; Long timestamp; diff --git a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java index 9856f6e1a26..10db83bec8a 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java +++ b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java @@ -10,8 +10,6 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; @@ -30,13 +28,18 @@ import org.prebid.server.auction.privacy.enforcement.TcfEnforcement; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.StreamUtil; import java.util.Collections; @@ -44,13 +47,11 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -/** - * Class dispatches event processing to all enabled reporters. - */ public class AnalyticsReporterDelegator { private static final Logger logger = LoggerFactory.getLogger(AnalyticsReporterDelegator.class); @@ -63,6 +64,8 @@ public class AnalyticsReporterDelegator { private final UserFpdActivityMask mask; private final Metrics metrics; private final double logSamplingRate; + private final Set globalEnabledAdapters; + private final JacksonMapper mapper; private final Set reporterVendorIds; private final Set reporterNames; @@ -72,7 +75,9 @@ public AnalyticsReporterDelegator(Vertx vertx, TcfEnforcement tcfEnforcement, UserFpdActivityMask userFpdActivityMask, Metrics metrics, - double logSamplingRate) { + double logSamplingRate, + Set globalEnabledAdapters, + JacksonMapper mapper) { this.vertx = Objects.requireNonNull(vertx); this.delegates = Objects.requireNonNull(delegates); @@ -80,6 +85,10 @@ public AnalyticsReporterDelegator(Vertx vertx, this.mask = Objects.requireNonNull(userFpdActivityMask); this.metrics = Objects.requireNonNull(metrics); this.logSamplingRate = logSamplingRate; + this.globalEnabledAdapters = CollectionUtils.isEmpty(globalEnabledAdapters) + ? Collections.emptySet() + : globalEnabledAdapters; + this.mapper = Objects.requireNonNull(mapper); reporterVendorIds = delegates.stream().map(AnalyticsReporter::vendorId).collect(Collectors.toSet()); reporterNames = delegates.stream().map(AnalyticsReporter::name).collect(Collectors.toSet()); @@ -126,8 +135,8 @@ private void delegateEvent(T event, } } else { final Throwable privacyEnforcementException = privacyEnforcementMapResult.cause(); - logger.error("Analytics TCF enforcement check failed for consentString: {0} and " - + "delegates with vendorIds {1}", privacyEnforcementException, + logger.error("Analytics TCF enforcement check failed for consentString: {} and " + + "delegates with vendorIds {}", privacyEnforcementException, tcfContext.getConsentString(), delegates); } } @@ -163,36 +172,84 @@ private static boolean isNotEmptyObjectNode(JsonNode analytics) { return analytics != null && analytics.isObject() && !analytics.isEmpty(); } - private static boolean isAllowedAdapter(T event, String adapter) { + private boolean isAllowedAdapter(T event, String adapter) { final ActivityInfrastructure activityInfrastructure; final ActivityInvocationPayload activityInvocationPayload; - if (event instanceof AuctionEvent auctionEvent) { - final AuctionContext auctionContext = auctionEvent.getAuctionContext(); - activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; - activityInvocationPayload = auctionContext != null - ? BidRequestActivityInvocationPayload.of( - activityInvocationPayload(adapter), - auctionContext.getBidRequest()) - : null; - } else if (event instanceof AmpEvent ampEvent) { - final AuctionContext auctionContext = ampEvent.getAuctionContext(); - activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; - activityInvocationPayload = auctionContext != null - ? BidRequestActivityInvocationPayload.of( - activityInvocationPayload(adapter), - auctionContext.getBidRequest()) - : null; - } else if (event instanceof NotificationEvent notificationEvent) { - activityInfrastructure = notificationEvent.getActivityInfrastructure(); - activityInvocationPayload = activityInvocationPayload(adapter); - } else { - activityInfrastructure = null; - activityInvocationPayload = null; + switch (event) { + case AuctionEvent auctionEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, auctionEvent.getAuctionContext())) { + return false; + } + final AuctionContext auctionContext = auctionEvent.getAuctionContext(); + activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; + activityInvocationPayload = auctionContext != null + ? BidRequestActivityInvocationPayload.of( + activityInvocationPayload(adapter), + auctionContext.getBidRequest()) + : null; + } + case AmpEvent ampEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, ampEvent.getAuctionContext())) { + return false; + } + + final AuctionContext auctionContext = ampEvent.getAuctionContext(); + activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; + activityInvocationPayload = auctionContext != null + ? BidRequestActivityInvocationPayload.of( + activityInvocationPayload(adapter), + auctionContext.getBidRequest()) + : null; + } + case NotificationEvent notificationEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, notificationEvent.getAccount())) { + return false; + } + activityInfrastructure = notificationEvent.getActivityInfrastructure(); + activityInvocationPayload = activityInvocationPayload(adapter); + } + case VideoEvent videoEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, videoEvent.getAuctionContext())) { + return false; + } + activityInfrastructure = null; + activityInvocationPayload = null; + } + case null, default -> { + activityInfrastructure = null; + activityInvocationPayload = null; + } } return isAllowedActivity(activityInfrastructure, Activity.REPORT_ANALYTICS, activityInvocationPayload); } + private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, AuctionContext auctionContext) { + return isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, + Optional.ofNullable(auctionContext) + .map(AuctionContext::getAccount) + .orElse(null)); + } + + private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, Account account) { + final Map modules = Optional.ofNullable(account) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .orElse(null); + + if (modules != null && modules.containsKey(adapter)) { + final ObjectNode moduleConfig = modules.get(adapter); + + if (moduleConfig == null || !moduleConfig.has("enabled")) { + return false; + } + + return !moduleConfig.get("enabled").asBoolean(); + } + + return !globalEnabledAdapters.contains(adapter); + } + private static ActivityInvocationPayload activityInvocationPayload(String adapterName) { return ActivityInvocationPayloadImpl.of(ComponentType.ANALYTICS, adapterName); } @@ -238,7 +295,7 @@ private BidRequest updateBidRequest(BidRequest bidRequest, final boolean disallowTransmitGeo = !isAllowedActivity(infrastructure, Activity.TRANSMIT_GEO, payload); final User user = bidRequest != null ? bidRequest.getUser() : null; - final User resolvedUser = mask.maskUser(user, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); + final User resolvedUser = mask.maskUser(user, disallowTransmitUfpd, disallowTransmitEids); final Device device = bidRequest != null ? bidRequest.getDevice() : null; final Device resolvedDevice = mask.maskDevice(device, disallowTransmitUfpd, disallowTransmitGeo); @@ -294,7 +351,8 @@ private static ObjectNode prepareAnalytics(ObjectNode analytics, String adapterN private void processEventByReporter(AnalyticsReporter analyticsReporter, T event) { final String reporterName = analyticsReporter.name(); - analyticsReporter.processEvent(event) + + analyticsReporter.processEvent(updateEventIfRequired(event, analyticsReporter.name())) .map(ignored -> processSuccess(event, reporterName)) .otherwise(exception -> processFail(exception, event, reporterName)); } @@ -318,24 +376,102 @@ private Future processFail(Throwable exception, T event, String report } private void updateMetricsByEventType(T event, String analyticsCode, MetricName result) { - final MetricName eventType; - - if (event instanceof AmpEvent) { - eventType = MetricName.event_amp; - } else if (event instanceof AuctionEvent) { - eventType = MetricName.event_auction; - } else if (event instanceof CookieSyncEvent) { - eventType = MetricName.event_cookie_sync; - } else if (event instanceof NotificationEvent) { - eventType = MetricName.event_notification; - } else if (event instanceof SetuidEvent) { - eventType = MetricName.event_setuid; - } else if (event instanceof VideoEvent) { - eventType = MetricName.event_video; - } else { - eventType = MetricName.event_unknown; - } + final MetricName eventType = switch (event) { + case AmpEvent ampEvent -> MetricName.event_amp; + case AuctionEvent auctionEvent -> MetricName.event_auction; + case CookieSyncEvent cookieSyncEvent -> MetricName.event_cookie_sync; + case NotificationEvent notificationEvent -> MetricName.event_notification; + case SetuidEvent setuidEvent -> MetricName.event_setuid; + case VideoEvent videoEvent -> MetricName.event_video; + case null, default -> MetricName.event_unknown; + }; metrics.updateAnalyticEventMetric(analyticsCode, eventType, result); } + + private T updateEventIfRequired(T event, String adapter) { + switch (event) { + case AuctionEvent auctionEvent -> { + final AuctionContext auctionContext = updateAuctionContext(auctionEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) auctionEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case AmpEvent ampEvent -> { + final AuctionContext auctionContext = updateAuctionContext(ampEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) ampEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case VideoEvent videoEvent -> { + final AuctionContext auctionContext = updateAuctionContext(videoEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) videoEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case null, default -> { + return event; + } + } + } + + private AuctionContext updateAuctionContext(AuctionContext context, String adapterName) { + final Map modules = Optional.ofNullable(context) + .map(AuctionContext::getAccount) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .orElse(null); + + if (modules != null && modules.containsKey(adapterName)) { + final ObjectNode moduleConfig = modules.get(adapterName); + if (moduleConfigContainsAdapterSpecificData(moduleConfig)) { + final ExtRequestPrebid extRequestPrebid = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .orElse(null); + + final JsonNode analyticsNode = extRequestPrebid != null ? extRequestPrebid.getAnalytics() : null; + + if (analyticsNode != null && analyticsNode.isObject()) { + final ObjectNode adapterNode = Optional.ofNullable((ObjectNode) analyticsNode.get(adapterName)) + .orElse(mapper.mapper().createObjectNode()); + + moduleConfig.fields().forEachRemaining(entry -> { + final String fieldName = entry.getKey(); + if (!"enabled".equals(fieldName) && !adapterNode.has(fieldName)) { + adapterNode.set(fieldName, entry.getValue()); + } + }); + + ((ObjectNode) analyticsNode).set(adapterName, adapterNode); + final ExtRequestPrebid updatedPrebid = extRequestPrebid.toBuilder() + .analytics(analyticsNode) + .build(); + final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); + final BidRequest updatedBidRequest = context.getBidRequest().toBuilder() + .ext(updatedExtRequest) + .build(); + return context.toBuilder() + .bidRequest(updatedBidRequest) + .build(); + } + } + } + + return null; + } + + private boolean moduleConfigContainsAdapterSpecificData(ObjectNode moduleConfig) { + if (moduleConfig != null) { + final Iterator fieldNames = moduleConfig.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (!"enabled".equals(fieldName)) { + return true; + } + } + } + + return false; + } } diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java new file mode 100644 index 00000000000..9c3252d4116 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -0,0 +1,268 @@ +package org.prebid.server.analytics.reporter.agma; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iabtcf.decoder.TCString; +import com.iabtcf.utils.IntIterable; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; +import org.prebid.server.analytics.reporter.agma.model.AgmaEvent; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.Initializable; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.zip.GZIPOutputStream; + +public class AgmaAnalyticsReporter implements AnalyticsReporter, Initializable { + + private static final Logger logger = LoggerFactory.getLogger(AgmaAnalyticsReporter.class); + + private final String url; + private final boolean compressToGzip; + private final long bufferTimeoutMs; + private final long httpTimeoutMs; + + private final EventBuffer buffer; + + private final Map accounts; + + private final Vertx vertx; + private final JacksonMapper jacksonMapper; + private final HttpClient httpClient; + private final Clock clock; + private final MultiMap headers; + + public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper jacksonMapper, + Clock clock, + HttpClient httpClient, + Vertx vertx) { + + this.accounts = agmaAnalyticsProperties.getAccounts(); + + this.url = HttpUtil.validateUrl(agmaAnalyticsProperties.getUrl()); + this.bufferTimeoutMs = agmaAnalyticsProperties.getBufferTimeoutMs(); + this.httpTimeoutMs = agmaAnalyticsProperties.getHttpTimeoutMs(); + this.compressToGzip = agmaAnalyticsProperties.isGzip(); + + this.buffer = new EventBuffer<>( + agmaAnalyticsProperties.getMaxEventsCount(), + agmaAnalyticsProperties.getBufferSize()); + + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.vertx = Objects.requireNonNull(vertx); + this.clock = Objects.requireNonNull(clock); + this.headers = makeHeaders(Objects.requireNonNull(prebidVersionProvider)); + } + + @Override + public void initialize(Promise initializePromise) { + vertx.setPeriodic(bufferTimeoutMs, ignored -> sendEvents(buffer.pollAll())); + initializePromise.complete(); + } + + @Override + public Future processEvent(T event) { + final Pair contextAndType = switch (event) { + case AuctionEvent auctionEvent -> Pair.of(auctionEvent.getAuctionContext(), "auction"); + case AmpEvent ampEvent -> Pair.of(ampEvent.getAuctionContext(), "amp"); + case VideoEvent videoEvent -> Pair.of(videoEvent.getAuctionContext(), "video"); + case null, default -> null; + }; + + if (contextAndType == null) { + return Future.succeededFuture(); + } + + final AuctionContext auctionContext = contextAndType.getLeft(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final TimeoutContext timeoutContext = auctionContext.getTimeoutContext(); + final PrivacyContext privacyContext = auctionContext.getPrivacyContext(); + + if (!allowedToSendEvent(bidRequest, privacyContext)) { + return Future.succeededFuture(); + } + + final String accountCode = Optional.ofNullable(bidRequest) + .map(AgmaAnalyticsReporter::getPublisherId) + .map(accounts::get) + .orElse(null); + + if (accountCode == null) { + return Future.succeededFuture(); + } + + final AgmaEvent agmaEvent = AgmaEvent.builder() + .eventType(contextAndType.getRight()) + .accountCode(accountCode) + .requestId(bidRequest.getId()) + .app(bidRequest.getApp()) + .site(bidRequest.getSite()) + .device(bidRequest.getDevice()) + .user(bidRequest.getUser()) + .startTime(ZonedDateTime.ofInstant( + Instant.ofEpochMilli(timeoutContext.getStartTime()), clock.getZone())) + .build(); + + final String eventString = jacksonMapper.encodeToString(agmaEvent); + buffer.put(eventString, eventString.length()); + sendEvents(buffer.pollToFlush()); + return Future.succeededFuture(); + } + + private boolean allowedToSendEvent(BidRequest bidRequest, PrivacyContext privacyContext) { + final TCString consent = Optional.ofNullable(privacyContext) + .map(PrivacyContext::getTcfContext) + .map(TcfContext::getConsent) + .or(() -> Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ExtUser::getConsent) + .map(AgmaAnalyticsReporter::decodeConsent)) + .orElse(null); + + if (consent == null) { + return false; + } + + final IntIterable purposesConsent = consent.getPurposesConsent(); + final IntIterable vendorConsent = consent.getVendorConsent(); + + final boolean isPurposeAllowed = purposesConsent.contains(PurposeCode.NINE.code()); + final boolean isVendorAllowed = vendorConsent.contains(vendorId()); + return isPurposeAllowed && isVendorAllowed; + } + + private static TCString decodeConsent(String consent) { + try { + return TCString.decode(consent); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static String getPublisherId(BidRequest bidRequest) { + final Site site = bidRequest.getSite(); + final App app = bidRequest.getApp(); + + final String publisherId = Optional.ofNullable(site).map(Site::getPublisher).map(Publisher::getId) + .or(() -> Optional.ofNullable(app).map(App::getPublisher).map(Publisher::getId)) + .orElse(null); + final String appSiteId = Optional.ofNullable(site).map(Site::getId) + .or(() -> Optional.ofNullable(app).map(App::getId)) + .or(() -> Optional.ofNullable(app).map(App::getBundle)) + .orElse(null); + + if (publisherId == null && appSiteId == null) { + return null; + } + + return StringUtils.isNotBlank(appSiteId) + ? String.format("%s_%s", StringUtils.defaultString(publisherId), appSiteId) + : publisherId; + } + + private void sendEvents(List events) { + if (events.isEmpty()) { + return; + } + final String payload = preparePayload(events); + final Future responseFuture = compressToGzip + ? httpClient.request(HttpMethod.POST, url, headers, gzip(payload), httpTimeoutMs) + : httpClient.request(HttpMethod.POST, url, headers, payload, httpTimeoutMs); + + responseFuture.onComplete(this::handleReportResponse); + } + + private static String preparePayload(List events) { + return "[" + String.join(",", events) + "]"; + } + + private static byte[] gzip(String value) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { + + gzip.write(value.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + + return obj.toByteArray(); + } catch (IOException e) { + throw new PreBidException("[agmaAnalytics] failed to compress, skip the events : " + e.getMessage()); + } + } + + private void handleReportResponse(AsyncResult result) { + if (result.failed()) { + logger.error("[agmaAnalytics] Failed to send events to endpoint {} with a reason: {}", + url, result.cause().getMessage()); + } else { + final HttpClientResponse httpClientResponse = result.result(); + final int statusCode = httpClientResponse.getStatusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + logger.error("[agmaAnalytics] Wrong code received {} instead of 200", statusCode); + } + } + } + + private MultiMap makeHeaders(PrebidVersionProvider versionProvider) { + final MultiMap headers = MultiMap.caseInsensitiveMultiMap() + .add(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) + .add(HttpUtil.X_PREBID_HEADER, versionProvider.getNameVersionRecord()); + + if (compressToGzip) { + headers.add(HttpHeaders.CONTENT_ENCODING, HttpHeaderValues.GZIP); + } + + return headers; + } + + @Override + public int vendorId() { + return 1122; + } + + @Override + public String name() { + return "agmaAnalytics"; + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java b/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java new file mode 100644 index 00000000000..d291fa0ba1c --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java @@ -0,0 +1,59 @@ +package org.prebid.server.analytics.reporter.agma; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class EventBuffer { + + private final Lock lock = new ReentrantLock(true); + + private List events = new ArrayList<>(); + + private long byteSize = 0; + + private final long maxEvents; + + private final long maxBytes; + + public EventBuffer(long maxEvents, long maxBytes) { + this.maxEvents = maxEvents; + this.maxBytes = maxBytes; + } + + public void put(T event, long eventSize) { + lock.lock(); + events.addLast(event); + byteSize += eventSize; + lock.unlock(); + } + + public List pollToFlush() { + List toFlush = Collections.emptyList(); + + lock.lock(); + if (events.size() >= maxEvents || byteSize >= maxBytes) { + toFlush = events; + reset(); + } + lock.unlock(); + + return toFlush; + } + + public List pollAll() { + lock.lock(); + final List polled = events; + reset(); + lock.unlock(); + + return polled; + } + + private void reset() { + byteSize = 0; + events = new ArrayList<>(); + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java new file mode 100644 index 00000000000..1d5a994dbe9 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class AgmaAccountAnalyticsProperties { + + String code; + + String publisherId; + + String siteAppId; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java new file mode 100644 index 00000000000..c1d5ed0c4ed --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java @@ -0,0 +1,26 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class AgmaAnalyticsProperties { + + String url; + + boolean gzip; + + Integer bufferSize; + + Integer maxEventsCount; + + Long bufferTimeoutMs; + + Long httpTimeoutMs; + + Map accounts; + +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java new file mode 100644 index 00000000000..51e385744bf --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java @@ -0,0 +1,38 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; + +@Value +@Builder +public class AgmaEvent { + + @JsonProperty("type") + String eventType; + + @JsonProperty("id") + String requestId; + + @JsonProperty("code") + String accountCode; + + Site site; + + App app; + + Device device; + + User user; + + //format 2023-02-01T00:00:00Z + @JsonProperty("created_at") + ZonedDateTime startTime; + +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java new file mode 100644 index 00000000000..5b87a0d4112 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java @@ -0,0 +1,402 @@ +package org.prebid.server.analytics.reporter.greenbids; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage; +import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsBid; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsPrebidExt; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsSource; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode; +import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.EncodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.time.Clock; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GreenbidsAnalyticsReporter implements AnalyticsReporter { + + private static final String BID_REQUEST_ANALYTICS_EXTENSION_NAME = "greenbids"; + private static final int RANGE_16_BIT_INTEGER_DIVISION_BASIS = 0x10000; + private static final String ANALYTICS_REQUEST_ORIGIN_HEADER = "X-Request-Origin"; + private static final String PREBID_SERVER_HEADER_VALUE = "Prebid Server"; + private static final Logger logger = LoggerFactory.getLogger(GreenbidsAnalyticsReporter.class); + + private final GreenbidsAnalyticsProperties greenbidsAnalyticsProperties; + private final JacksonMapper jacksonMapper; + private final HttpClient httpClient; + private final Clock clock; + private final PrebidVersionProvider prebidVersionProvider; + + public GreenbidsAnalyticsReporter( + GreenbidsAnalyticsProperties greenbidsAnalyticsProperties, + JacksonMapper jacksonMapper, + HttpClient httpClient, + Clock clock, + PrebidVersionProvider prebidVersionProvider) { + this.greenbidsAnalyticsProperties = Objects.requireNonNull(greenbidsAnalyticsProperties); + this.httpClient = Objects.requireNonNull(httpClient); + this.clock = Objects.requireNonNull(clock); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + } + + @Override + public Future processEvent(T event) { + final AuctionContext auctionContext; + final BidResponse bidResponse; + + if (event instanceof AmpEvent ampEvent) { + auctionContext = ampEvent.getAuctionContext(); + bidResponse = ampEvent.getBidResponse(); + } else if (event instanceof AuctionEvent auctionEvent) { + auctionContext = auctionEvent.getAuctionContext(); + bidResponse = auctionEvent.getBidResponse(); + } else { + return Future.failedFuture(new PreBidException("Unprocessable event received")); + } + + if (bidResponse == null || auctionContext == null) { + return Future.failedFuture(new PreBidException("Bid response or auction context cannot be null")); + } + + final GreenbidsPrebidExt greenbidsBidRequestExt = parseBidRequestExt(auctionContext.getBidRequest()); + + if (greenbidsBidRequestExt == null) { + return Future.succeededFuture(); + } + + final String greenbidsId = UUID.randomUUID().toString(); + final String billingId = UUID.randomUUID().toString(); + + if (!isSampled(greenbidsBidRequestExt.getGreenbidsSampling(), greenbidsId)) { + return Future.succeededFuture(); + } + + final String commonMessageJson; + try { + final CommonMessage commonMessage = createBidMessage( + auctionContext, + bidResponse, + greenbidsId, + billingId, + greenbidsBidRequestExt); + commonMessageJson = jacksonMapper.encodeToString(commonMessage); + } catch (PreBidException e) { + return Future.failedFuture(e); + } catch (EncodeException e) { + return Future.failedFuture(new PreBidException("Failed to encode as JSON: ", e)); + } + + final MultiMap headers = MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .add(ANALYTICS_REQUEST_ORIGIN_HEADER, PREBID_SERVER_HEADER_VALUE); + + Optional.ofNullable(auctionContext.getBidRequest()) + .map(BidRequest::getDevice) + .map(Device::getUa) + .ifPresent(userAgent -> headers.add(HttpUtil.USER_AGENT_HEADER, userAgent)); + + final Future responseFuture = httpClient.post( + greenbidsAnalyticsProperties.getAnalyticsServerUrl(), + headers, + commonMessageJson, + greenbidsAnalyticsProperties.getTimeoutMs()); + + return responseFuture.compose(this::processAnalyticServerResponse); + } + + private GreenbidsPrebidExt parseBidRequestExt(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .filter(this::isNotEmptyObjectNode) + .map(analytics -> (ObjectNode) analytics.get(BID_REQUEST_ANALYTICS_EXTENSION_NAME)) + .map(this::toGreenbidsPrebidExt) + .orElse(null); + } + + private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) { + try { + return jacksonMapper.mapper().treeToValue(adapterNode, GreenbidsPrebidExt.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding bid request analytics extension: " + e.getMessage(), e); + } + } + + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + + private Future processAnalyticServerResponse(HttpClientResponse response) { + final int responseStatusCode = response.getStatusCode(); + if (responseStatusCode >= 200 && responseStatusCode < 300) { + return Future.succeededFuture(); + } + return Future.failedFuture(new PreBidException("Unexpected response status: " + response.getStatusCode())); + } + + private boolean isSampled(Double samplingRate, String greenbidsId) { + if (samplingRate == null) { + logger.warn("Warning: Sampling rate is not defined in request. Set sampling at " + + greenbidsAnalyticsProperties.getDefaultSamplingRate()); + return true; + } + + if (samplingRate < 0 || samplingRate > 1) { + logger.warn("Warning: Sampling rate must be between 0 and 1"); + return true; + } + + final double exploratorySamplingRate = samplingRate + * greenbidsAnalyticsProperties.getExploratorySamplingSplit(); + final double throttledSamplingRate = samplingRate + * (1.0 - greenbidsAnalyticsProperties.getExploratorySamplingSplit()); + + final int hashInt = Integer.parseInt( + greenbidsId.substring(greenbidsId.length() - 4), 16); + final boolean isPrimarySampled = hashInt < exploratorySamplingRate * RANGE_16_BIT_INTEGER_DIVISION_BASIS; + final boolean isExtraSampledOutOfExploration = hashInt >= (1 - throttledSamplingRate) + * RANGE_16_BIT_INTEGER_DIVISION_BASIS; + + return isPrimarySampled || isExtraSampledOutOfExploration; + } + + private CommonMessage createBidMessage( + AuctionContext auctionContext, + BidResponse bidResponse, + String greenbidsId, + String billingId, + GreenbidsPrebidExt greenbidsImpExt) { + final Optional bidRequest = Optional.ofNullable(auctionContext.getBidRequest()); + + final List imps = bidRequest + .map(BidRequest::getImp) + .filter(CollectionUtils::isNotEmpty) + .orElseThrow(() -> new PreBidException("Impressions list should not be empty")); + + final long auctionElapsed = bidRequest + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAuctiontimestamp) + .map(timestamp -> clock.millis() - timestamp).orElse(0L); + + final Map seatsWithBids = getSeatsWithBids(bidResponse); + + final Map seatsWithNonBids = getSeatsWithNonBids(auctionContext); + + final List adUnitsWithBidResponses = imps.stream().map(imp -> createAdUnit( + imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur())).toList(); + + final String auctionId = bidRequest + .map(BidRequest::getId) + .orElse(null); + + final String referrer = bidRequest + .map(BidRequest::getSite) + .map(Site::getPage) + .orElse(null); + + final Double greenbidsSamplingRate = Optional.ofNullable(greenbidsImpExt.getGreenbidsSampling()) + .orElse(greenbidsAnalyticsProperties.getDefaultSamplingRate()); + + return CommonMessage.builder() + .version(greenbidsAnalyticsProperties.getAnalyticsServerVersion()) + .auctionId(auctionId) + .referrer(referrer) + .sampling(greenbidsSamplingRate) + .prebidServer(prebidVersionProvider.getNameVersionRecord()) + .greenbidsId(greenbidsId) + .pbuid(greenbidsImpExt.getPbuid()) + .billingId(billingId) + .adUnits(adUnitsWithBidResponses) + .auctionElapsed(auctionElapsed) + .build(); + } + + private static Map getSeatsWithBids(BidResponse bidResponse) { + return Stream.ofNullable(bidResponse.getSeatbid()) + .flatMap(Collection::stream) + .filter(seatBid -> !seatBid.getBid().isEmpty()) + .collect( + Collectors.toMap( + SeatBid::getSeat, + seatBid -> seatBid.getBid().getFirst(), + (existing, replacement) -> existing)); + } + + private static Map getSeatsWithNonBids(AuctionContext auctionContext) { + return auctionContext.getBidRejectionTrackers().entrySet().stream() + .map(entry -> toSeatNonBid(entry.getKey(), entry.getValue())) + .filter(seatNonBid -> !seatNonBid.getNonBid().isEmpty()) + .collect( + Collectors.toMap( + SeatNonBid::getSeat, + seatNonBid -> seatNonBid.getNonBid().getFirst(), + (existing, replacement) -> existing)); + } + + private static SeatNonBid toSeatNonBid(String bidder, BidRejectionTracker bidRejectionTracker) { + final List nonBids = bidRejectionTracker.getRejectionReasons().entrySet().stream() + .map(entry -> NonBid.of(entry.getKey(), entry.getValue())) + .toList(); + + return SeatNonBid.of(bidder, nonBids); + } + + private GreenbidsAdUnit createAdUnit( + Imp imp, + Map seatsWithBids, + Map seatsWithNonBids, + String currency) { + final ExtBanner extBanner = getExtBanner(imp.getBanner()); + final Video video = imp.getVideo(); + final Native nativeObject = imp.getXNative(); + + final MediaTypes mediaTypes = MediaTypes.of(extBanner, video, nativeObject); + + final ObjectNode impExt = imp.getExt(); + final String adUnitCode = imp.getId(); + + final ExtImpPrebid impExtPrebid = Optional.ofNullable(impExt) + .map(ext -> ext.get("prebid")) + .map(this::extImpPrebid) + .orElseThrow(() -> new PreBidException("imp.ext.prebid should not be empty")); + + final GreenbidsUnifiedCode greenbidsUnifiedCode = getGpid(impExt) + .or(() -> getStoredRequestId(impExtPrebid)) + .orElseGet(() -> GreenbidsUnifiedCode.of( + adUnitCode, GreenbidsSource.AD_UNIT_CODE_SOURCE.getValue())); + + final List bids = extractBidders( + imp.getId(), seatsWithBids, seatsWithNonBids, impExtPrebid, currency); + + return GreenbidsAdUnit.builder() + .code(adUnitCode) + .unifiedCode(greenbidsUnifiedCode) + .mediaTypes(mediaTypes) + .bids(bids) + .build(); + } + + private static ExtBanner getExtBanner(Banner banner) { + if (banner == null) { + return null; + } + + final List> sizes = Optional.ofNullable(banner.getFormat()) + .filter(formats -> !formats.isEmpty()) + .map(formats -> formats.stream() + .map(format -> List.of(format.getW(), format.getH())) + .collect(Collectors.toList())) + .orElse(banner.getW() != null && banner.getH() != null + ? List.of(List.of(banner.getW(), banner.getH())) + : Collections.emptyList()); + + return ExtBanner.builder() + .sizes(sizes) + .pos(banner.getPos()) + .name(banner.getId()) + .build(); + } + + private List extractBidders( + String impId, + Map seatsWithBids, + Map seatsWithNonBids, + ExtImpPrebid impExtPrebid, + String currency) { + final ObjectNode bidders = impExtPrebid.getBidder(); + + return Stream.concat( + seatsWithBids.entrySet().stream() + .filter(entry -> entry.getValue().getImpid().equals(impId)) + .map(entry -> GreenbidsBid.ofBid( + entry.getKey(), entry.getValue(), bidders.get(entry.getKey()), currency)), + seatsWithNonBids.entrySet().stream() + .filter(entry -> entry.getValue().getImpId().equals(impId)) + .map(entry -> GreenbidsBid.ofNonBid( + entry.getKey(), entry.getValue(), bidders.get(entry.getKey()), currency))) + .toList(); + } + + private static Optional getGpid(ObjectNode impExt) { + return Optional.ofNullable(impExt) + .map(ext -> ext.get("gpid")) + .map(JsonNode::asText) + .map(gpid -> + GreenbidsUnifiedCode.of(gpid, GreenbidsSource.GPID_SOURCE.getValue())); + } + + private Optional getStoredRequestId(ExtImpPrebid extImpPrebid) { + return Optional.ofNullable(extImpPrebid.getStoredrequest()) + .map(ExtStoredRequest::getId) + .map(storedRequestId -> + GreenbidsUnifiedCode.of( + storedRequestId, GreenbidsSource.STORED_REQUEST_ID_SOURCE.getValue())); + } + + private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { + try { + return jacksonMapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e); + } + } + + @Override + public int vendorId() { + return 0; + } + + @Override + public String name() { + return "greenbids"; + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/CommonMessage.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/CommonMessage.java new file mode 100644 index 00000000000..bdde5b3772d --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/CommonMessage.java @@ -0,0 +1,38 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class CommonMessage { + + String version; + + @JsonProperty("auctionId") + String auctionId; + + String referrer; + + Double sampling; + + @JsonProperty("prebidServer") + String prebidServer; + + @JsonProperty("greenbidsId") + String greenbidsId; + + String pbuid; + + @JsonProperty("billingId") + String billingId; + + @JsonProperty("adUnits") + List adUnits; + + @JsonProperty("auctionElapsed") + Long auctionElapsed; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExtBanner.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExtBanner.java new file mode 100644 index 00000000000..26bddc79615 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExtBanner.java @@ -0,0 +1,17 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class ExtBanner { + + List> sizes; + + Integer pos; + + String name; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java new file mode 100644 index 00000000000..ce3ae13231a --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java @@ -0,0 +1,22 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class GreenbidsAdUnit { + + String code; + + @JsonProperty("unifiedCode") + GreenbidsUnifiedCode unifiedCode; + + @JsonProperty("mediaTypes") + MediaTypes mediaTypes; + + List bids; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAnalyticsProperties.java new file mode 100644 index 00000000000..fcca8d3a758 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAnalyticsProperties.java @@ -0,0 +1,21 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class GreenbidsAnalyticsProperties { + + Double exploratorySamplingSplit; + + Double defaultSamplingRate; + + String analyticsServerVersion; + + String analyticsServerUrl; + + Long configurationRefreshDelayMs; + + Long timeoutMs; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java new file mode 100644 index 00000000000..f65e2f1bf80 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java @@ -0,0 +1,51 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.response.Bid; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; + +import java.math.BigDecimal; + +@Builder(toBuilder = true) +@Value +public class GreenbidsBid { + + String bidder; + + @JsonProperty("isTimeout") + Boolean isTimeout; + + @JsonProperty("hasBid") + Boolean hasBid; + + JsonNode params; + + BigDecimal cpm; + + String currency; + + public static GreenbidsBid ofBid(String seat, Bid bid, JsonNode params, String currency) { + return GreenbidsBid.builder() + .bidder(seat) + .isTimeout(false) + .hasBid(bid != null) + .params(params) + .cpm(bid.getPrice()) + .currency(currency) + .build(); + } + + public static GreenbidsBid ofNonBid(String seat, NonBid nonBid, JsonNode params, String currency) { + return GreenbidsBid.builder() + .bidder(seat) + .isTimeout(nonBid.getStatusCode() == BidRejectionReason.ERROR_TIMED_OUT) + .hasBid(false) + .params(params) + .currency(currency) + .build(); + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsPrebidExt.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsPrebidExt.java new file mode 100644 index 00000000000..d5983205898 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsPrebidExt.java @@ -0,0 +1,13 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class GreenbidsPrebidExt { + + String pbuid; + + @JsonProperty("greenbidsSampling") + Double greenbidsSampling; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsSource.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsSource.java new file mode 100644 index 00000000000..d9dffb0378c --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsSource.java @@ -0,0 +1,18 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +public enum GreenbidsSource { + + GPID_SOURCE("gpidSource"), + STORED_REQUEST_ID_SOURCE("storedRequestIdSource"), + AD_UNIT_CODE_SOURCE("adUnitCodeSource"); + + private final String value; + + GreenbidsSource(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsUnifiedCode.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsUnifiedCode.java new file mode 100644 index 00000000000..3e1683f2e5b --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsUnifiedCode.java @@ -0,0 +1,11 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class GreenbidsUnifiedCode { + + String value; + + String source; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/MediaTypes.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/MediaTypes.java new file mode 100644 index 00000000000..a9821b786ba --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/MediaTypes.java @@ -0,0 +1,17 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediaTypes { + + ExtBanner banner; + + Video video; + + @JsonProperty("native") + Native nativeObject; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java index 61931743085..63f8a364dd6 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java @@ -1,8 +1,6 @@ package org.prebid.server.analytics.reporter.log; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.AuctionEvent; @@ -12,6 +10,8 @@ import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.analytics.reporter.log.model.LogEvent; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import java.util.Objects; @@ -30,25 +30,18 @@ public LogAnalyticsReporter(JacksonMapper mapper) { @Override public Future processEvent(T event) { - final LogEvent logEvent; - - if (event instanceof AmpEvent ampEvent) { - logEvent = LogEvent.of("/openrtb2/amp", ampEvent.getBidResponse()); - } else if (event instanceof AuctionEvent auctionEvent) { - logEvent = LogEvent.of("/openrtb2/auction", auctionEvent.getBidResponse()); - } else if (event instanceof CookieSyncEvent cookieSyncEvent) { - logEvent = LogEvent.of("/cookie_sync", cookieSyncEvent.getBidderStatus()); - } else if (event instanceof NotificationEvent notificationEvent) { - logEvent = LogEvent.of("/event", notificationEvent.getType() + notificationEvent.getBidId()); - } else if (event instanceof SetuidEvent setuidEvent) { - logEvent = LogEvent.of( + final LogEvent logEvent = switch (event) { + case AmpEvent ampEvent -> LogEvent.of("/openrtb2/amp", ampEvent.getBidResponse()); + case AuctionEvent auctionEvent -> LogEvent.of("/openrtb2/auction", auctionEvent.getBidResponse()); + case CookieSyncEvent cookieSyncEvent -> LogEvent.of("/cookie_sync", cookieSyncEvent.getBidderStatus()); + case NotificationEvent notificationEvent -> + LogEvent.of("/event", notificationEvent.getType() + notificationEvent.getBidId()); + case SetuidEvent setuidEvent -> LogEvent.of( "/setuid", setuidEvent.getBidder() + ":" + setuidEvent.getUid() + ":" + setuidEvent.getSuccess()); - } else if (event instanceof VideoEvent videoEvent) { - logEvent = LogEvent.of("/openrtb2/video", videoEvent.getBidResponse()); - } else { - logEvent = LogEvent.of("unknown", null); - } + case VideoEvent videoEvent -> LogEvent.of("/openrtb2/video", videoEvent.getBidResponse()); + case null, default -> LogEvent.of("unknown", null); + }; logger.debug(mapper.encodeToString(logEvent)); diff --git a/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java index 47e5a506a5d..0bddcbe05ca 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java @@ -2,9 +2,8 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Future; +import io.vertx.core.Promise; import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.analytics.AnalyticsReporter; @@ -20,10 +19,12 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.Initializable; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.Arrays; import java.util.Collections; @@ -88,23 +89,15 @@ private static String buildEventEndpointUrl(String endpoint, EventType eventType @Override public Future processEvent(T event) { - final EventType eventType; - - if (event instanceof AmpEvent) { - eventType = EventType.amp; - } else if (event instanceof AuctionEvent) { - eventType = EventType.auction; - } else if (event instanceof CookieSyncEvent) { - eventType = EventType.cookiesync; - } else if (event instanceof NotificationEvent) { - eventType = EventType.notification; - } else if (event instanceof SetuidEvent) { - eventType = EventType.setuid; - } else if (event instanceof VideoEvent) { - eventType = EventType.video; - } else { - eventType = null; - } + final EventType eventType = switch (event) { + case AmpEvent ampEvent -> EventType.amp; + case AuctionEvent auctionEvent -> EventType.auction; + case CookieSyncEvent cookieSyncEvent -> EventType.cookiesync; + case NotificationEvent notificationEvent -> EventType.notification; + case SetuidEvent setuidEvent -> EventType.setuid; + case VideoEvent videoEvent -> EventType.video; + case null, default -> null; + }; if (eventType != null) { eventHandlers.get(eventType).handle(event); @@ -124,9 +117,10 @@ public String name() { } @Override - public void initialize() { + public void initialize(Promise initializePromise) { vertx.setPeriodic(configurationRefreshDelay, id -> fetchRemoteConfig()); fetchRemoteConfig(); + initializePromise.tryComplete(); } void shutdown() { @@ -134,7 +128,7 @@ void shutdown() { } private void fetchRemoteConfig() { - logger.info("[pubstack] Updating config: {0}", pubstackConfig); + logger.info("[pubstack] Updating config: {}", pubstackConfig); httpClient.get(makeEventEndpointUrl(pubstackConfig.getEndpoint(), pubstackConfig.getScopeId()), timeout) .map(this::processRemoteConfigurationResponse) .onComplete(this::updateConfigsOnChange); @@ -156,7 +150,7 @@ private PubstackConfig processRemoteConfigurationResponse(HttpClientResponse res private void updateConfigsOnChange(AsyncResult asyncConfigResult) { if (asyncConfigResult.failed()) { - logger.error("[pubstask] Fail to fetch remote configuration: {0}", asyncConfigResult.cause().getMessage()); + logger.error("[pubstask] Fail to fetch remote configuration: {}", asyncConfigResult.cause().getMessage()); } else if (!Objects.equals(pubstackConfig, asyncConfigResult.result())) { final PubstackConfig pubstackConfig = asyncConfigResult.result(); eventHandlers.values().forEach(PubstackEventHandler::reportEvents); diff --git a/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java index 235677ad61e..3dc927ba0bf 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java @@ -7,14 +7,14 @@ import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -89,7 +89,7 @@ public void handle(T event) { public void reportEvents() { if (enabled) { - reportEventsOnCondition(events -> events.get().size() > 0, events); + reportEventsOnCondition(events -> !events.get().isEmpty(), events); } } @@ -118,7 +118,7 @@ private boolean reportEventsOnCondition(Predicate conditionToSend, T cond sendEvents(events); } } catch (Exception exception) { - logger.error("[pubstack] Failed to send analytics report to endpoint {0} with a reason {1}", + logger.error("[pubstack] Failed to send analytics report to endpoint {} with a reason {}", endpoint, exception.getMessage()); } finally { lockOnSend.unlock(); @@ -163,13 +163,13 @@ private static byte[] gzip(String value) { private void handleReportResponse(AsyncResult result) { if (result.failed()) { - logger.error("[pubstack] Failed to send events to endpoint {0} with a reason: {1}", + logger.error("[pubstack] Failed to send events to endpoint {} with a reason: {}", endpoint, result.cause().getMessage()); } else { final HttpClientResponse httpClientResponse = result.result(); final int statusCode = httpClientResponse.getStatusCode(); if (statusCode != HttpResponseStatus.OK.code()) { - logger.error("[pubstack] Wrong code received {0} instead of 200", statusCode); + logger.error("[pubstack] Wrong code received {} instead of 200", statusCode); } } } @@ -179,7 +179,7 @@ private long setReportTtlTimer() { } private void sendOnTimer() { - final boolean requestWasSent = reportEventsOnCondition(events -> events.get().size() > 0, events); + final boolean requestWasSent = reportEventsOnCondition(events -> !events.get().isEmpty(), events); if (!requestWasSent) { setReportTtlTimer(); } diff --git a/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java new file mode 100644 index 00000000000..15344b28576 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java @@ -0,0 +1,125 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class AnalyticsTagsEnricher { + + private AnalyticsTagsEnricher() { + } + + public static AuctionContext enrichWithAnalyticsTags(AuctionContext context) { + final boolean clientDetailsEnabled = isClientDetailsEnabled(context); + if (!clientDetailsEnabled) { + return context; + } + + final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::isAllowClientDetails) + .orElse(false); + + if (!allowClientDetails) { + return addClientDetailsWarning(context); + } + + final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); + + if (extAnalyticsTags == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .analytics(ExtAnalytics.of(extAnalyticsTags)) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static boolean isClientDetailsEnabled(AuctionContext context) { + final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .orElse(null); + + if (notObjectNode(analytics)) { + return false; + } + + final JsonNode options = analytics.get("options"); + if (notObjectNode(options)) { + return false; + } + + final JsonNode enableClientDetails = options.get("enableclientdetails"); + return enableClientDetails != null + && enableClientDetails.isBoolean() + && enableClientDetails.asBoolean(); + } + + private static boolean notObjectNode(JsonNode jsonNode) { + return jsonNode == null || !jsonNode.isObject(); + } + + private static AuctionContext addClientDetailsWarning(AuctionContext context) { + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + + final Map> warnings = ext + .map(ExtBidResponse::getWarnings) + .orElse(Collections.emptyMap()); + final List prebidWarnings = ObjectUtils.defaultIfNull( + warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), + Collections.emptyList()); + + final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); + updatedPrebidWarnings.add(ExtBidderError.of( + BidderError.Type.generic.getCode(), + "analytics.options.enableclientdetails not enabled for account")); + final Map> updatedWarnings = new HashMap<>(warnings); + updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .warnings(updatedWarnings) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } +} diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 30aed97692f..1c0b837bb4e 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -18,6 +18,7 @@ import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -41,13 +42,12 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.BidderSeatBidInfo; -import org.prebid.server.cache.CacheService; +import org.prebid.server.cache.CoreCacheService; import org.prebid.server.cache.model.CacheContext; import org.prebid.server.cache.model.CacheInfo; import org.prebid.server.cache.model.CacheServiceResult; +import org.prebid.server.cache.model.CacheTtl; import org.prebid.server.cache.model.DebugHttpCall; -import org.prebid.server.deals.model.DeepDebugLog; -import org.prebid.server.deals.model.TxnLog; import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.InvalidRequestException; @@ -61,7 +61,6 @@ import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -81,13 +80,11 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidResponseFledge; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; -import org.prebid.server.proto.openrtb.ext.response.ExtDebugPgmetrics; import org.prebid.server.proto.openrtb.ext.response.ExtDebugTrace; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; import org.prebid.server.proto.openrtb.ext.response.ExtResponseCache; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; import org.prebid.server.proto.openrtb.ext.response.ExtTraceActivityInfrastructure; -import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; @@ -98,7 +95,6 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountTargetingConfig; import org.prebid.server.settings.model.VideoStoredDataResult; -import org.prebid.server.util.LineItemUtil; import org.prebid.server.util.StreamUtil; import org.prebid.server.vast.VastModifier; @@ -126,8 +122,11 @@ public class BidResponseCreator { private static final Integer DEFAULT_BID_LIMIT_MIN = 1; private static final Integer MAX_TARGETING_KEY_LENGTH = 11; private static final String DEFAULT_TARGETING_KEY_PREFIX = "hb"; + public static final String DEFAULT_DEBUG_KEY = "prebid"; + private static final String TARGETING_ENV_APP_VALUE = "mobile-app"; + private static final String TARGETING_ENV_AMP_VALUE = "amp"; - private final CacheService cacheService; + private final CoreCacheService coreCacheService; private final BidderCatalog bidderCatalog; private final VastModifier vastModifier; private final EventsService eventsService; @@ -139,12 +138,13 @@ public class BidResponseCreator { private final int truncateAttrChars; private final Clock clock; private final JacksonMapper mapper; + private final CacheTtl mediaTypeCacheTtl; private final String cacheHost; private final String cachePath; private final String cacheAssetUrlTemplate; - public BidResponseCreator(CacheService cacheService, + public BidResponseCreator(CoreCacheService coreCacheService, BidderCatalog bidderCatalog, VastModifier vastModifier, EventsService eventsService, @@ -155,9 +155,10 @@ public BidResponseCreator(CacheService cacheService, CategoryMappingService categoryMappingService, int truncateAttrChars, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + CacheTtl mediaTypeCacheTtl) { - this.cacheService = Objects.requireNonNull(cacheService); + this.coreCacheService = Objects.requireNonNull(coreCacheService); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.vastModifier = Objects.requireNonNull(vastModifier); this.eventsService = Objects.requireNonNull(eventsService); @@ -169,10 +170,11 @@ public BidResponseCreator(CacheService cacheService, this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); + this.mediaTypeCacheTtl = Objects.requireNonNull(mediaTypeCacheTtl); - cacheHost = Objects.requireNonNull(cacheService.getEndpointHost()); - cachePath = Objects.requireNonNull(cacheService.getEndpointPath()); - cacheAssetUrlTemplate = Objects.requireNonNull(cacheService.getCachedAssetURLTemplate()); + cacheHost = Objects.requireNonNull(coreCacheService.getEndpointHost()); + cachePath = Objects.requireNonNull(coreCacheService.getEndpointPath()); + cacheAssetUrlTemplate = Objects.requireNonNull(coreCacheService.getCachedAssetURLTemplate()); } private static int validateTruncateAttrChars(int truncateAttrChars) { @@ -182,50 +184,63 @@ private static int validateTruncateAttrChars(int truncateAttrChars) { return truncateAttrChars; } - /** - * Creates an OpenRTB {@link BidResponse} from the bids supplied by the bidder, - * including processing of winning bids with cache IDs. - */ + Future createOnSkippedAuction(AuctionContext auctionContext, List seatBids) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + + final ExtBidResponse extBidResponse = ExtBidResponse.builder() + .warnings(extractContextWarnings(auctionContext)) + .tmaxrequest(bidRequest.getTmax()) + .build(); + + final List cur = bidRequest.getCur(); + final BidResponse bidResponse = BidResponse.builder() + .id(bidRequest.getId()) + .cur(CollectionUtils.isNotEmpty(cur) ? cur.getFirst() : null) + .seatbid(ListUtils.emptyIfNull(seatBids)) + .ext(extBidResponse) + .build(); + + return Future.succeededFuture(bidResponse); + } + Future create(AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, Map bidderToMultiBids) { - final List auctionParticipations = auctionContext.getAuctionParticipations(); - final List imps = auctionContext.getBidRequest().getImp(); + return videoStoredDataResult(auctionContext) + .compose(videoStoredData -> create(videoStoredData, auctionContext, cacheInfo, bidderToMultiBids)) + .map(bidResponse -> populateSeatNonBid(auctionContext, bidResponse)); + } + + private Future create(VideoStoredDataResult videoStoredDataResult, + AuctionContext auctionContext, + BidRequestCacheInfo cacheInfo, + Map bidderToMultiBids) { + final EventsContext eventsContext = createEventsContext(auctionContext); - final List bidderResponses = auctionParticipations.stream() + final List bidderResponses = auctionContext.getAuctionParticipations().stream() .filter(auctionParticipation -> !auctionParticipation.isRequestBlocked()) .map(AuctionParticipation::getBidderResponse) .toList(); - return videoStoredDataResult(auctionContext).compose(videoStoredDataResult -> - invokeProcessedBidderResponseHooks( - updateBids(bidderResponses, videoStoredDataResult, auctionContext, eventsContext, imps), - auctionContext) - - .compose(updatedResponses -> - invokeAllProcessedBidResponsesHook(updatedResponses, auctionContext)) - - .compose(updatedResponses -> - createCategoryMapping(auctionContext, updatedResponses)) - - .compose(categoryMappingResult -> cacheBidsAndCreateResponse( - toBidderResponseInfos(categoryMappingResult, imps), - auctionContext, - cacheInfo, - bidderToMultiBids, - videoStoredDataResult, - eventsContext)) - - .map(bidResponse -> populateSeatNonBid(auctionContext, bidResponse))); + return updateBids(bidderResponses, videoStoredDataResult, auctionContext, eventsContext) + .compose(updatedResponses -> invokeProcessedBidderResponseHooks(updatedResponses, auctionContext)) + .compose(updatedResponses -> invokeAllProcessedBidResponsesHook(updatedResponses, auctionContext)) + .compose(updatedResponses -> createCategoryMapping(auctionContext, updatedResponses)) + .compose(categoryMappingResult -> cacheBidsAndCreateResponse( + toBidderResponseInfos(categoryMappingResult, cacheInfo, auctionContext), + auctionContext, + cacheInfo, + bidderToMultiBids, + videoStoredDataResult, + eventsContext)); } - private List updateBids(List bidderResponses, - VideoStoredDataResult videoStoredDataResult, - AuctionContext auctionContext, - EventsContext eventsContext, - List imps) { + private Future> updateBids(List bidderResponses, + VideoStoredDataResult videoStoredDataResult, + AuctionContext auctionContext, + EventsContext eventsContext) { final List result = new ArrayList<>(); @@ -238,12 +253,8 @@ private List updateBids(List bidderResponses, final Bid receivedBid = bidderBid.getBid(); final BidType bidType = bidderBid.getType(); - final Imp correspondingImp = correspondingImp(receivedBid, imps); - final ExtDealLine extDealLine = LineItemUtil.extDealLineFrom(receivedBid, correspondingImp, mapper); - final String lineItemId = extDealLine != null ? extDealLine.getLineItemId() : null; - final Bid updatedBid = updateBid( - receivedBid, bidType, bidder, videoStoredDataResult, auctionContext, eventsContext, lineItemId); + receivedBid, bidType, bidder, videoStoredDataResult, auctionContext, eventsContext); modifiedBidderBids.add(bidderBid.toBuilder().bid(updatedBid).build()); } @@ -251,7 +262,7 @@ private List updateBids(List bidderResponses, result.add(bidderResponse.with(modifiedSeatBid)); } - return result; + return Future.succeededFuture(result); } private Bid updateBid(Bid bid, @@ -259,8 +270,7 @@ private Bid updateBid(Bid bid, String bidder, VideoStoredDataResult videoStoredDataResult, AuctionContext auctionContext, - EventsContext eventsContext, - String lineItemId) { + EventsContext eventsContext) { final Account account = auctionContext.getAccount(); final List debugWarnings = auctionContext.getDebugWarnings(); @@ -277,8 +287,7 @@ private Bid updateBid(Bid bid, account, eventsContext, effectiveBidId, - debugWarnings, - lineItemId)) + debugWarnings)) .ext(updateBidExt( bid, bidType, @@ -287,8 +296,7 @@ private Bid updateBid(Bid bid, videoStoredDataResult, eventsContext, generatedBidId, - effectiveBidId, - lineItemId)) + effectiveBidId)) .build(); } @@ -298,8 +306,7 @@ private String updateBidAdm(Bid bid, Account account, EventsContext eventsContext, String effectiveBidId, - List debugWarnings, - String lineItemId) { + List debugWarnings) { final String bidAdm = bid.getAdm(); return BidType.video.equals(bidType) @@ -310,8 +317,7 @@ private String updateBidAdm(Bid bid, effectiveBidId, account.getId(), eventsContext, - debugWarnings, - lineItemId) + debugWarnings) : bidAdm; } @@ -322,8 +328,7 @@ private ObjectNode updateBidExt(Bid bid, VideoStoredDataResult videoStoredDataResult, EventsContext eventsContext, String generatedBidId, - String effectiveBidId, - String lineItemId) { + String effectiveBidId) { final ExtBidPrebid updatedExtBidPrebid = updateBidExtPrebid( bid, @@ -333,8 +338,7 @@ private ObjectNode updateBidExt(Bid bid, videoStoredDataResult, eventsContext, generatedBidId, - effectiveBidId, - lineItemId); + effectiveBidId); final ObjectNode existingBidExt = bid.getExt(); final ObjectNode updatedBidExt = mapper.mapper().createObjectNode(); @@ -355,11 +359,10 @@ private ExtBidPrebid updateBidExtPrebid(Bid bid, VideoStoredDataResult videoStoredDataResult, EventsContext eventsContext, String generatedBidId, - String effectiveBidId, - String lineItemId) { + String effectiveBidId) { final Video storedVideo = videoStoredDataResult.getImpIdToStoredVideo().get(bid.getImpid()); - final Events events = createEvents(bidder, account, effectiveBidId, eventsContext, lineItemId); + final Events events = createEvents(bidder, account, effectiveBidId, eventsContext); final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid.getExt()).orElse(null); final ExtBidPrebid.ExtBidPrebidBuilder extBidPrebidBuilder = getExtPrebid(bid.getExt(), ExtBidPrebid.class) @@ -385,8 +388,11 @@ private static boolean isEmptyBidderResponses(List bidderRes } private List toBidderResponseInfos(CategoryMappingResult categoryMappingResult, - List imps) { + BidRequestCacheInfo cacheInfo, + AuctionContext auctionContext) { + final List imps = auctionContext.getBidRequest().getImp(); + final Account account = auctionContext.getAccount(); final List result = new ArrayList<>(); final List bidderResponses = categoryMappingResult.getBidderResponses(); @@ -399,7 +405,7 @@ private List toBidderResponseInfos(CategoryMappingResult cat for (final BidderBid bidderBid : seatBid.getBids()) { final Bid bid = bidderBid.getBid(); final BidType type = bidderBid.getType(); - final BidInfo bidInfo = toBidInfo(bid, type, imps, bidder, categoryMappingResult); + final BidInfo bidInfo = toBidInfo(bid, type, imps, bidder, categoryMappingResult, cacheInfo, account); bidInfos.add(bidInfo); } @@ -420,18 +426,18 @@ private BidInfo toBidInfo(Bid bid, BidType type, List imps, String bidder, - CategoryMappingResult categoryMappingResult) { + CategoryMappingResult categoryMappingResult, + BidRequestCacheInfo cacheInfo, + Account account) { final Imp correspondingImp = correspondingImp(bid, imps); - final ExtDealLine extDealLine = LineItemUtil.extDealLineFrom(bid, correspondingImp, mapper); - final String lineItemId = extDealLine != null ? extDealLine.getLineItemId() : null; - return BidInfo.builder() .bid(bid) .bidType(type) .bidder(bidder) .correspondingImp(correspondingImp) - .lineItemId(lineItemId) + .ttl(resolveBannerTtl(bid, correspondingImp, cacheInfo, account)) + .videoTtl(type == BidType.video ? resolveVideoTtl(bid, correspondingImp, cacheInfo, account) : null) .category(categoryMappingResult.getCategory(bid)) .satisfiedPriority(categoryMappingResult.isBidSatisfiesPriority(bid)) .build(); @@ -451,6 +457,33 @@ private static Optional correspondingImp(String impId, List imps) { .findFirst(); } + private Integer resolveBannerTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer bidTtl = bid.getExp(); + final Integer impTtl = imp != null ? imp.getExp() : null; + + return ObjectUtils.firstNonNull( + bidTtl, + impTtl, + cacheInfo.getCacheBidsTtl(), + accountAuctionConfig != null ? accountAuctionConfig.getBannerCacheTtl() : null, + mediaTypeCacheTtl.getBannerCacheTtl()); + + } + + private Integer resolveVideoTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer bidTtl = bid.getExp(); + final Integer impTtl = imp != null ? imp.getExp() : null; + + return ObjectUtils.firstNonNull( + bidTtl, + impTtl, + cacheInfo.getCacheVideoBidsTtl(), + accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null, + mediaTypeCacheTtl.getVideoCacheTtl()); + } + private Future> invokeProcessedBidderResponseHooks(List bidderResponses, AuctionContext auctionContext) { @@ -474,12 +507,11 @@ private static BidderResponse rejectBidderResponseOrProceed( HookStageExecutionResult stageResult, BidderResponse bidderResponse) { - final List bids = - stageResult.isShouldReject() ? Collections.emptyList() : stageResult.getPayload().bids(); + final List bids = stageResult.isShouldReject() + ? Collections.emptyList() + : stageResult.getPayload().bids(); - return bidderResponse - .with(bidderResponse.getSeatBid() - .with(bids)); + return bidderResponse.with(bidderResponse.getSeatBid().with(bids)); } private Future createCategoryMapping(AuctionContext auctionContext, @@ -527,7 +559,7 @@ private Future cacheBidsAndCreateResponse(List return Future.succeededFuture(BidResponse.builder() .id(bidRequest.getId()) - .cur(bidRequest.getCur().get(0)) + .cur(bidRequest.getCur().getFirst()) .nbr(0) // signal "Unknown Error" .seatbid(Collections.emptyList()) .ext(extBidResponse) @@ -535,10 +567,9 @@ private Future cacheBidsAndCreateResponse(List } final ExtRequestTargeting targeting = targeting(bidRequest); - final TxnLog txnLog = auctionContext.getTxnLog(); final List bidderResponseInfos = toBidderResponseWithTargetingBidInfos( - bidderResponses, bidderToMultiBids, preferDeals(targeting), txnLog); + bidderResponses, bidderToMultiBids, preferDeals(targeting)); final Set bidInfos = bidderResponseInfos.stream() .map(BidderResponseInfo::getSeatBid) @@ -553,8 +584,6 @@ private Future cacheBidsAndCreateResponse(List .filter(bidInfo -> bidInfo.getTargetingInfo().isWinningBid()) .collect(Collectors.toSet()); - updateSentToClientTxnLog(txnLog, bidInfos); - final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() ? winningBidInfos : bidInfos; return cacheBids(bidsToCache, auctionContext, cacheInfo, eventsContext) @@ -581,8 +610,7 @@ private static boolean preferDeals(ExtRequestTargeting targeting) { private List toBidderResponseWithTargetingBidInfos( List bidderResponses, Map bidderToMultiBids, - boolean preferDeals, - TxnLog txnLog) { + boolean preferDeals) { final Map> bidderResponseToReducedBidInfos = bidderResponses.stream() .collect(Collectors.toMap( @@ -611,13 +639,6 @@ private List toBidderResponseWithTargetingBidInfos( .ifPresent(winningBids::add); } - final Map> impIdToLineItemIds = impIdToBidderToBidInfos.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - impIdToBidderToBidInfoEntry -> toLineItemIds(impIdToBidderToBidInfoEntry.getValue().values()))); - - updateTopMatchAndLostAuctionLineItemsMetric(winningBids, txnLog, impIdToLineItemIds); - return bidderResponseToReducedBidInfos.entrySet().stream() .map(responseToBidInfos -> injectBidInfoWithTargeting( responseToBidInfos.getKey(), @@ -652,33 +673,6 @@ private List sortReducedBidInfo(List bidInfos, int limit, bool .toList(); } - private static Set toLineItemIds(Collection> bidInfos) { - return bidInfos.stream() - .flatMap(Collection::stream) - .map(BidInfo::getLineItemId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - /** - * Updates sent to client as top match and auction lost to line item metric. - */ - private static void updateTopMatchAndLostAuctionLineItemsMetric(Set winningBidInfos, - TxnLog txnLog, - Map> impToLineItemIds) { - for (BidInfo winningBidInfo : winningBidInfos) { - final String winningLineItemId = winningBidInfo.getLineItemId(); - if (winningLineItemId != null) { - txnLog.lineItemSentToClientAsTopMatch().add(winningLineItemId); - - final String impIdOfWinningBid = winningBidInfo.getBid().getImpid(); - impToLineItemIds.get(impIdOfWinningBid).stream() - .filter(lineItemId -> !Objects.equals(lineItemId, winningLineItemId)) - .forEach(lineItemId -> txnLog.lostAuctionToLineItems().get(lineItemId).add(winningLineItemId)); - } - } - } - private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, List bidderBidInfos, Map bidderToMultiBids, @@ -744,16 +738,6 @@ private static List injectTargeting(List bidderImpIdBidInfos, return result; } - /** - * Increments sent to client metrics for each bid with deal. - */ - private static void updateSentToClientTxnLog(TxnLog txnLog, Set bidInfos) { - bidInfos.stream() - .map(BidInfo::getLineItemId) - .filter(Objects::nonNull) - .forEach(lineItemId -> txnLog.lineItemsSentToClient().add(lineItemId)); - } - /** * Returns {@link ExtBidResponse} object, populated with response time, errors and debug info (if requested) * from all bidders. @@ -853,13 +837,10 @@ private static ExtResponseDebug toExtResponseDebug(List bidd : null; final BidRequest bidRequest = debugEnabled ? auctionContext.getBidRequest() : null; - - final ExtDebugPgmetrics extDebugPgmetrics = debugEnabled ? toExtDebugPgmetrics( - auctionContext.getTxnLog()) : null; final ExtDebugTrace extDebugTrace = toExtDebugTrace(auctionContext); - return ObjectUtils.anyNotNull(httpCalls, bidRequest, extDebugPgmetrics, extDebugTrace) - ? ExtResponseDebug.of(httpCalls, bidRequest, extDebugPgmetrics, extDebugTrace) + return ObjectUtils.anyNotNull(httpCalls, bidRequest, extDebugTrace) + ? ExtResponseDebug.of(httpCalls, bidRequest, extDebugTrace) : null; } @@ -881,13 +862,11 @@ private Future cacheBids(Set bidsToCache, .toList(); final CacheContext cacheContext = CacheContext.builder() - .cacheBidsTtl(cacheInfo.getCacheBidsTtl()) - .cacheVideoBidsTtl(cacheInfo.getCacheVideoBidsTtl()) .shouldCacheBids(cacheInfo.isShouldCacheBids()) .shouldCacheVideoBids(cacheInfo.isShouldCacheVideoBids()) .build(); - return cacheService.cacheBidsOpenrtb(bidsValidToBeCached, auctionContext, cacheContext, eventsContext) + return coreCacheService.cacheBidsOpenrtb(bidsValidToBeCached, auctionContext, cacheContext, eventsContext) .map(cacheResult -> addNotCachedBids(cacheResult, bidsToCache)); } @@ -963,54 +942,16 @@ private static ExtHttpCall toExtHttpCall(DebugHttpCall debugHttpCall) { .build(); } - private static ExtDebugPgmetrics toExtDebugPgmetrics(TxnLog txnLog) { - final ExtDebugPgmetrics extDebugPgmetrics = ExtDebugPgmetrics.builder() - .matchedDomainTargeting(nullIfEmpty(txnLog.lineItemsMatchedDomainTargeting())) - .matchedWholeTargeting(nullIfEmpty(txnLog.lineItemsMatchedWholeTargeting())) - .matchedTargetingFcapped(nullIfEmpty(txnLog.lineItemsMatchedTargetingFcapped())) - .matchedTargetingFcapLookupFailed(nullIfEmpty(txnLog.lineItemsMatchedTargetingFcapLookupFailed())) - .readyToServe(nullIfEmpty(txnLog.lineItemsReadyToServe())) - .pacingDeferred(nullIfEmpty(txnLog.lineItemsPacingDeferred())) - .sentToBidder(nullIfEmpty(txnLog.lineItemsSentToBidder())) - .sentToBidderAsTopMatch(nullIfEmpty(txnLog.lineItemsSentToBidderAsTopMatch())) - .receivedFromBidder(nullIfEmpty(txnLog.lineItemsReceivedFromBidder())) - .responseInvalidated(nullIfEmpty(txnLog.lineItemsResponseInvalidated())) - .sentToClient(nullIfEmpty(txnLog.lineItemsSentToClient())) - .sentToClientAsTopMatch(nullIfEmpty(txnLog.lineItemSentToClientAsTopMatch())) - .build(); - return extDebugPgmetrics.equals(ExtDebugPgmetrics.EMPTY) ? null : extDebugPgmetrics; - } - private static ExtDebugTrace toExtDebugTrace(AuctionContext auctionContext) { - final DeepDebugLog deepDebugLog = auctionContext.getDeepDebugLog(); - - final boolean dealsTraceEnabled = deepDebugLog.isDeepDebugEnabled(); - final boolean activityInfrastructureTraceEnabled = auctionContext.getDebugContext().getTraceLevel() != null; - if (!dealsTraceEnabled && !activityInfrastructureTraceEnabled) { + if (auctionContext.getDebugContext().getTraceLevel() == null) { return null; } - final List entries = dealsTraceEnabled ? deepDebugLog.entries() : null; - final List dealsTrace = dealsTraceEnabled - ? entries.stream() - .filter(extTraceDeal -> StringUtils.isEmpty(extTraceDeal.getLineItemId())) - .toList() - : null; - final Map> lineItemsTrace = dealsTraceEnabled - ? entries.stream() - .filter(extTraceDeal -> StringUtils.isNotEmpty(extTraceDeal.getLineItemId())) - .collect(Collectors.groupingBy(ExtTraceDeal::getLineItemId, Collectors.toList())) - : null; - - final List activityInfrastructureTrace = activityInfrastructureTraceEnabled - ? new ArrayList<>(auctionContext.getActivityInfrastructure().debugTrace()) - : null; + final List activityInfrastructureTrace = + new ArrayList<>(auctionContext.getActivityInfrastructure().debugTrace()); - return CollectionUtils.isNotEmpty(entries) || CollectionUtils.isNotEmpty(activityInfrastructureTrace) - ? ExtDebugTrace.of( - CollectionUtils.isEmpty(dealsTrace) ? null : dealsTrace, - MapUtils.isEmpty(lineItemsTrace) ? null : lineItemsTrace, - CollectionUtils.isEmpty(activityInfrastructureTrace) ? null : activityInfrastructureTrace) + return CollectionUtils.isNotEmpty(activityInfrastructureTrace) + ? ExtDebugTrace.of(activityInfrastructureTrace) : null; } @@ -1102,7 +1043,7 @@ private static Map> extractPrebidErrors(VideoStored final List collectedErrors = Stream.concat(contextErrors.stream(), storedErrors.stream()).toList(); - return Collections.singletonMap(PREBID_EXT, collectedErrors); + return Collections.singletonMap(DEFAULT_DEBUG_KEY, collectedErrors); } /** @@ -1160,6 +1101,7 @@ private static Map> toExtBidderWarnings( List bidderResponses, AuctionContext auctionContext, Map> bidWarnings) { + final Map> warnings = new HashMap<>(); warnings.putAll(extractContextWarnings(auctionContext)); @@ -1176,7 +1118,7 @@ private static Map> extractContextWarnings(AuctionC return contextWarnings.isEmpty() ? Collections.emptyMap() - : Collections.singletonMap(PREBID_EXT, contextWarnings); + : Collections.singletonMap(DEFAULT_DEBUG_KEY, contextWarnings); } /** @@ -1244,7 +1186,7 @@ private BidResponse toBidResponse(List bidderResponseInfos, return BidResponse.builder() .id(bidRequest.getId()) - .cur(bidRequest.getCur().get(0)) + .cur(bidRequest.getCur().getFirst()) .seatbid(seatBids) .ext(extBidResponse) .build(); @@ -1387,13 +1329,11 @@ private Bid toBid(BidInfo bidInfo, final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; - final boolean isApp = bidRequest.getApp() != null; - final Map targetingKeywords; final String bidderCode = targetingInfo.getBidderCode(); if (shouldIncludeTargetingInResponse(targeting, bidInfo.getTargetingInfo())) { final TargetingKeywordsCreator keywordsCreator = resolveKeywordsCreator( - bidType, targeting, isApp, bidRequest, account, bidWarnings); + bidType, targeting, bidRequest, account, bidWarnings); final boolean isWinningBid = targetingInfo.isWinningBid(); final String categoryDuration = bidInfo.getCategory(); @@ -1427,7 +1367,9 @@ private Bid toBid(BidInfo bidInfo, originalBidExt != null ? originalBidExt.deepCopy() : mapper.mapper().createObjectNode(); updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(updatedExtBidPrebid)); - final Integer ttl = cacheInfo != null ? ObjectUtils.max(cacheInfo.getTtl(), cacheInfo.getVideoTtl()) : null; + final Integer ttl = Optional.ofNullable(cacheInfo) + .map(info -> ObjectUtils.max(cacheInfo.getTtl(), cacheInfo.getVideoTtl())) + .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVideoTtl())); return bid.toBuilder() .ext(updatedBidExt) @@ -1598,36 +1540,29 @@ private static String integrationFrom(AuctionContext auctionContext) { private Events createEvents(String bidder, Account account, String bidId, - EventsContext eventsContext, - String lineItemId) { - - if (!eventsContext.isEnabledForAccount()) { - return null; - } + EventsContext eventsContext) { - return eventsContext.isEnabledForRequest() || StringUtils.isNotEmpty(lineItemId) + return eventsContext.isEnabledForAccount() && eventsContext.isEnabledForRequest() ? eventsService.createEvent( bidId, bidder, account.getId(), - lineItemId, - eventsContext.isEnabledForRequest(), + true, eventsContext) : null; } private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { final Map keywordsCreatorByBidType = - keywordsCreatorByBidType(targeting, isApp, bidRequest, account, bidWarnings); + keywordsCreatorByBidType(targeting, bidRequest, account, bidWarnings); return keywordsCreatorByBidType.getOrDefault( - bidType, keywordsCreator(targeting, isApp, bidRequest, account, bidWarnings)); + bidType, keywordsCreator(targeting, bidRequest, account, bidWarnings)); } /** @@ -1635,7 +1570,6 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, * instance if it is present. */ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1643,7 +1577,7 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, final JsonNode priceGranularityNode = targeting.getPricegranularity(); return priceGranularityNode == null || priceGranularityNode.isNull() ? null - : createKeywordsCreator(targeting, isApp, priceGranularityNode, bidRequest, account, bidWarnings); + : createKeywordsCreator(targeting, priceGranularityNode, bidRequest, account, bidWarnings); } /** @@ -1652,7 +1586,6 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, */ private Map keywordsCreatorByBidType( ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1668,21 +1601,21 @@ private Map keywordsCreatorByBidType( final boolean isBannerNull = banner == null || banner.isNull(); if (!isBannerNull) { result.put( - BidType.banner, createKeywordsCreator(targeting, isApp, banner, bidRequest, account, bidWarnings)); + BidType.banner, createKeywordsCreator(targeting, banner, bidRequest, account, bidWarnings)); } final ObjectNode video = mediaTypePriceGranularity.getVideo(); final boolean isVideoNull = video == null || video.isNull(); if (!isVideoNull) { result.put( - BidType.video, createKeywordsCreator(targeting, isApp, video, bidRequest, account, bidWarnings)); + BidType.video, createKeywordsCreator(targeting, video, bidRequest, account, bidWarnings)); } final ObjectNode xNative = mediaTypePriceGranularity.getXNative(); final boolean isNativeNull = xNative == null || xNative.isNull(); if (!isNativeNull) { result.put( - BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, account, bidWarnings) + BidType.xNative, createKeywordsCreator(targeting, xNative, bidRequest, account, bidWarnings) ); } @@ -1690,7 +1623,6 @@ BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, ac } private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targeting, - boolean isApp, JsonNode priceGranularity, BidRequest bidRequest, Account account, @@ -1698,13 +1630,20 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe final int resolvedTruncateAttrChars = resolveTruncateAttrChars(targeting, account); final String resolveKeyPrefix = resolveAndValidateKeyPrefix( bidRequest, account, resolvedTruncateAttrChars, bidWarnings); + + final String env = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAmp) + .map(ignored -> TARGETING_ENV_AMP_VALUE) + .orElse(bidRequest.getApp() == null ? null : TARGETING_ENV_APP_VALUE); + return TargetingKeywordsCreator.create( parsePriceGranularity(priceGranularity), BooleanUtils.toBoolean(targeting.getIncludewinners()), BooleanUtils.toBoolean(targeting.getIncludebidderkeys()), BooleanUtils.toBoolean(targeting.getAlwaysincludedeals()), BooleanUtils.isTrue(targeting.getIncludeformat()), - isApp, + env, resolvedTruncateAttrChars, cacheHost, cachePath, @@ -1824,13 +1763,6 @@ private static Set nullIfEmpty(Set set) { return Collections.unmodifiableSet(set); } - private static Map nullIfEmpty(Map map) { - if (map.isEmpty()) { - return null; - } - return Collections.unmodifiableMap(map); - } - /** * Creates {@link ExtBidPrebidVideo} from bid extension. */ diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java new file mode 100644 index 00000000000..8134662fcd9 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -0,0 +1,240 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.PbsUtil; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidsAdjuster { + + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; + + private final ResponseBidValidator responseBidValidator; + private final CurrencyConversionService currencyService; + private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final PriceFloorEnforcer priceFloorEnforcer; + private final DsaEnforcer dsaEnforcer; + private final JacksonMapper mapper; + + public BidsAdjuster(ResponseBidValidator responseBidValidator, + CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + JacksonMapper mapper) { + + this.responseBidValidator = Objects.requireNonNull(responseBidValidator); + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); + this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); + this.mapper = Objects.requireNonNull(mapper); + } + + public List validateAndAdjustBids(List auctionParticipations, + AuctionContext auctionContext, + BidderAliases aliases) { + + return auctionParticipations.stream() + .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) + .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) + .map(auctionParticipation -> priceFloorEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getAccount(), + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .map(auctionParticipation -> dsaEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .toList(); + } + + private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, + AuctionContext auctionContext, + BidderAliases aliases) { + + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + final List errors = new ArrayList<>(seatBid.getErrors()); + final List warnings = new ArrayList<>(seatBid.getWarnings()); + + final List requestCurrencies = bidRequest.getCur(); + if (requestCurrencies.size() > 1) { + warnings.add(BidderError.badInput( + "a single currency (" + requestCurrencies.getFirst() + ") has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency.")); + } + + final List bids = seatBid.getBids(); + final List validBids = new ArrayList<>(bids.size()); + + for (final BidderBid bid : bids) { + final ValidationResult validationResult = responseBidValidator.validate( + bid, + bidderResponse.getBidder(), + auctionContext, + aliases); + + if (validationResult.hasWarnings() || validationResult.hasErrors()) { + errors.add(makeValidationBidderError(bid.getBid(), validationResult)); + } + + if (!validationResult.hasErrors()) { + validBids.add(bid); + } + } + + final BidderResponse resultBidderResponse = bidderResponse.with( + seatBid.toBuilder() + .bids(validBids) + .errors(errors) + .warnings(warnings) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { + final String validationErrors = Stream.concat( + validationResult.getErrors().stream().map(message -> "Error: " + message), + validationResult.getWarnings().stream().map(message -> "Warning: " + message)) + .collect(Collectors.joining(". ")); + + final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); + return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); + } + + private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, + BidRequest bidRequest) { + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + + final List bidderBids = seatBid.getBids(); + if (bidderBids.isEmpty()) { + return auctionParticipation; + } + + final List updatedBidderBids = new ArrayList<>(bidderBids.size()); + final List errors = new ArrayList<>(seatBid.getErrors()); + final String adServerCurrency = bidRequest.getCur().getFirst(); + + for (final BidderBid bidderBid : bidderBids) { + try { + final BidderBid updatedBidderBid = + updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); + updatedBidderBids.add(updatedBidderBid); + } catch (PreBidException e) { + errors.add(BidderError.generic(e.getMessage())); + } + } + + final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() + .bids(updatedBidderBids) + .errors(errors) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, + BidderResponse bidderResponse, + BidRequest bidRequest, + String adServerCurrency) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( + price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); + + final BigDecimal priceAdjustmentFactor = + bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); + + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); + + final Bid.BidBuilder bidBuilder = bid.toBuilder(); + if (adjustedPrice.compareTo(price) != 0) { + bidBuilder.price(adjustedPrice); + } + + if (!updatedBidExt.isEmpty()) { + bidBuilder.ext(updatedBidExt); + } + + return bidderBid.toBuilder().bid(bidBuilder.build()).build(); + } + + private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { + final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); + if (adjustmentFactors == null) { + return null; + } + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); + + return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); + } + + private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); + return prebid != null ? prebid.getBidadjustmentfactors() : null; + } + + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { + return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 + ? price.multiply(priceAdjustmentFactor) + : price; + } + + private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); + if (StringUtils.isNotBlank(bidCurrency)) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); + } + } + + private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { + node.set(propertyName, propertyValue); + } +} diff --git a/src/main/java/org/prebid/server/auction/DsaEnforcer.java b/src/main/java/org/prebid/server/auction/DsaEnforcer.java index ee992fae6ed..6719829cc56 100644 --- a/src/main/java/org/prebid/server/auction/DsaEnforcer.java +++ b/src/main/java/org/prebid/server/auction/DsaEnforcer.java @@ -1,6 +1,6 @@ package org.prebid.server.auction; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Regs; import com.iab.openrtb.response.Bid; @@ -12,19 +12,35 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.DsaPublisherRender; +import org.prebid.server.proto.openrtb.ext.request.DsaRequired; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.response.DsaAdvertiserRender; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; public class DsaEnforcer { private static final String DSA_EXT = "dsa"; - private static final Set DSA_REQUIRED = Set.of(2, 3); + private static final Set DSA_REQUIRED = Set.of( + DsaRequired.REQUIRED.getValue(), + DsaRequired.REQUIRED_ONLINE_PLATFORM.getValue()); + private static final int MAX_DSA_FIELD_LENGTH = 100; + + private final JacksonMapper mapper; + + public DsaEnforcer(JacksonMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } public AuctionParticipation enforce(BidRequest bidRequest, AuctionParticipation auctionParticipation, @@ -34,7 +50,7 @@ public AuctionParticipation enforce(BidRequest bidRequest, final BidderSeatBid seatBid = ObjectUtil.getIfNotNull(bidderResponse, BidderResponse::getSeatBid); final List bidderBids = ObjectUtil.getIfNotNull(seatBid, BidderSeatBid::getBids); - if (CollectionUtils.isEmpty(bidderBids) || !isDsaValidationRequired(bidRequest)) { + if (CollectionUtils.isEmpty(bidderBids)) { return auctionParticipation; } @@ -44,9 +60,19 @@ public AuctionParticipation enforce(BidRequest bidRequest, for (BidderBid bidderBid : bidderBids) { final Bid bid = bidderBid.getBid(); - if (!isValid(bid)) { - warnings.add(BidderError.invalidBid("Bid \"%s\" missing DSA".formatted(bid.getId()))); - rejectionTracker.reject(bid.getImpid(), BidRejectionReason.GENERAL); + final ExtBidDsa dsaResponse = Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get(DSA_EXT)) + .map(this::getDsaResponse) + .orElse(null); + + try { + validateFieldLength(dsaResponse); + if (isDsaValidationRequired(bidRequest)) { + validateDsa(bidRequest, dsaResponse); + } + } catch (PreBidException e) { + warnings.add(BidderError.invalidBid("Bid \"%s\": %s".formatted(bid.getId(), e.getMessage()))); + rejectionTracker.reject(bid.getImpid(), BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); updatedBidderBids.remove(bidderBid); } } @@ -71,9 +97,52 @@ private static boolean isDsaValidationRequired(BidRequest bidRequest) { .orElse(false); } - private boolean isValid(Bid bid) { - final ObjectNode bidExt = bid.getExt(); - return bidExt != null && bidExt.hasNonNull(DSA_EXT) && !bidExt.get(DSA_EXT).isEmpty(); + private static void validateDsa(BidRequest bidRequest, ExtBidDsa dsaResponse) { + if (dsaResponse == null) { + throw new PreBidException("DSA object missing when required"); + } + + final Integer adRender = dsaResponse.getAdRender(); + final Integer pubRender = bidRequest.getRegs().getExt().getDsa().getPubRender(); + + if (pubRender == null) { + return; + } + + if (pubRender == DsaPublisherRender.WILL_RENDER.getValue() + && adRender != null && adRender == DsaAdvertiserRender.WILL_RENDER.getValue()) { + throw new PreBidException("DSA publisher and buyer both signal will render"); + } + + if (pubRender == DsaPublisherRender.NOT_RENDER.getValue() + && (adRender == null || adRender == DsaAdvertiserRender.NOT_RENDER.getValue())) { + throw new PreBidException("DSA publisher and buyer both signal will not render"); + } + } + + private static void validateFieldLength(ExtBidDsa dsaResponse) { + if (dsaResponse == null) { + return; + } + + if (!hasValidLength(dsaResponse.getBehalf())) { + throw new PreBidException("DSA behalf exceeds limit of 100 chars"); + } + if (!hasValidLength(dsaResponse.getPaid())) { + throw new PreBidException("DSA paid exceeds limit of 100 chars"); + } + } + + private static boolean hasValidLength(String value) { + return value == null || value.length() <= MAX_DSA_FIELD_LENGTH; + } + + private ExtBidDsa getDsaResponse(JsonNode dsaExt) { + try { + return mapper.mapper().convertValue(dsaExt, ExtBidDsa.class); + } catch (IllegalArgumentException e) { + return null; + } } } diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 43b2515c2a6..66b34a8df46 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -2,13 +2,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; -import com.iab.openrtb.request.Deal; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; @@ -21,8 +18,6 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.map.CaseInsensitiveMap; @@ -34,7 +29,6 @@ import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -47,28 +41,26 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.StoredResponseResult; -import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcementService; import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcementService; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.HttpBidderRequester; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.DealsService; -import org.prebid.server.deals.events.ApplicationEventService; -import org.prebid.server.deals.model.TxnLog; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.ExecutionAction; import org.prebid.server.hooks.execution.model.ExecutionStatus; @@ -78,15 +70,14 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionOutcome; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; -import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.CriteriaLogManager; import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -98,7 +89,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; @@ -109,27 +99,11 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; -import org.prebid.server.proto.openrtb.ext.request.TraceLevel; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; -import org.prebid.server.proto.openrtb.ext.response.ExtModules; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.settings.model.Account; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.LineItemUtil; -import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.ListUtil; +import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; import java.time.Clock; @@ -145,15 +119,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -/** - * Executes an OpenRTB v2.5-2.6 Auction. - */ public class ExchangeService { private static final Logger logger = LoggerFactory.getLogger(ExchangeService.class); @@ -162,8 +129,6 @@ public class ExchangeService { private static final String PREBID_EXT = "prebid"; private static final String BIDDER_EXT = "bidder"; private static final String TID_EXT = "tid"; - private static final String ORIGINAL_BID_CPM = "origbidcpm"; - private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; private static final String ALL_BIDDERS_CONFIG = "*"; private static final Integer DEFAULT_MULTIBID_LIMIT_MIN = 1; private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; @@ -173,9 +138,9 @@ public class ExchangeService { private final double logSamplingRate; private final BidderCatalog bidderCatalog; private final StoredResponseProcessor storedResponseProcessor; - private final DealsService dealsService; private final PrivacyEnforcementService privacyEnforcementService; private final FpdResolver fpdResolver; + private final ImpAdjuster impAdjuster; private final SupplyChainResolver supplyChainResolver; private final DebugResolver debugResolver; private final MediaTypeProcessor mediaTypeProcessor; @@ -184,17 +149,13 @@ public class ExchangeService { private final TimeoutFactory timeoutFactory; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final HttpBidderRequester httpBidderRequester; - private final ResponseBidValidator responseBidValidator; - private final CurrencyConversionService currencyService; private final BidResponseCreator bidResponseCreator; - private final ApplicationEventService applicationEventService; private final BidResponsePostProcessor bidResponsePostProcessor; private final HookStageExecutor hookStageExecutor; private final HttpInteractionLogger httpInteractionLogger; private final PriceFloorAdjuster priceFloorAdjuster; - private final PriceFloorEnforcer priceFloorEnforcer; - private final DsaEnforcer dsaEnforcer; - private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final PriceFloorProcessor priceFloorProcessor; + private final BidsAdjuster bidsAdjuster; private final Metrics metrics; private final Clock clock; private final JacksonMapper mapper; @@ -204,9 +165,9 @@ public class ExchangeService { public ExchangeService(double logSamplingRate, BidderCatalog bidderCatalog, StoredResponseProcessor storedResponseProcessor, - DealsService dealsService, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, MediaTypeProcessor mediaTypeProcessor, @@ -215,17 +176,13 @@ public ExchangeService(double logSamplingRate, TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, - ApplicationEventService applicationEventService, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + PriceFloorProcessor priceFloorProcessor, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -235,9 +192,9 @@ public ExchangeService(double logSamplingRate, this.logSamplingRate = logSamplingRate; this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); - this.dealsService = dealsService; this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.impAdjuster = Objects.requireNonNull(impAdjuster); this.supplyChainResolver = Objects.requireNonNull(supplyChainResolver); this.debugResolver = Objects.requireNonNull(debugResolver); this.mediaTypeProcessor = Objects.requireNonNull(mediaTypeProcessor); @@ -246,17 +203,13 @@ public ExchangeService(double logSamplingRate, this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.httpBidderRequester = Objects.requireNonNull(httpBidderRequester); - this.responseBidValidator = Objects.requireNonNull(responseBidValidator); - this.currencyService = Objects.requireNonNull(currencyService); this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator); this.bidResponsePostProcessor = Objects.requireNonNull(bidResponsePostProcessor); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); - this.applicationEventService = applicationEventService; this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); - this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); - this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); - this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); + this.bidsAdjuster = Objects.requireNonNull(bidsAdjuster); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -264,14 +217,11 @@ public ExchangeService(double logSamplingRate, this.enabledStrictAppSiteDoohValidation = enabledStrictAppSiteDoohValidation; } - /** - * Runs an auction: delegates request to applicable bidders, gathers responses from them and constructs final - * response containing returned bids and additional information in extensions. - */ public Future holdAuction(AuctionContext context) { return processAuctionRequest(context) .compose(this::invokeResponseHooks) - .map(this::enrichWithHooksDebugInfo) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) .map(this::updateHooksMetrics); } @@ -301,15 +251,11 @@ private Future runAuction(AuctionContext receivedContext) { return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), timeout) .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses)) - .compose(storedResponseResult -> extractAuctionParticipations( - receivedContext, storedResponseResult, aliases, bidderToMultiBid)) - - .map(auctionParticipations -> matchAndPopulateDeals(auctionParticipations, aliases, receivedContext)) - .map(auctionParticipations -> postProcessDeals(auctionParticipations, receivedContext)) - .map(auctionParticipations -> fillContext(receivedContext, auctionParticipations)) + .compose(storedResponseResult -> + extractAuctionParticipations(receivedContext, storedResponseResult, aliases, bidderToMultiBid) + .map(receivedContext::with)) .map(context -> updateRequestMetric(context, uidsCookie, aliases, account, requestTypeMetric)) - .compose(context -> CompositeFuture.join( context.getAuctionParticipations().stream() .map(auctionParticipation -> processAndRequestBids( @@ -323,39 +269,42 @@ private Future runAuction(AuctionContext receivedContext) { .map(CompositeFuture::list) .map(storedResponseProcessor::updateStoredBidResponse) .map(auctionParticipations -> storedResponseProcessor.mergeWithBidderResponses( - auctionParticipations, storedAuctionResponses, bidRequest.getImp())) + auctionParticipations, + storedAuctionResponses, + bidRequest.getImp(), + context.getBidRejectionTrackers())) .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings)) - .map(auctionParticipations -> validateAndAdjustBids(auctionParticipations, context, aliases)) + .map(auctionParticipations -> + bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases)) .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases)) .map(context::with)) // produce response from bidder results .compose(context -> bidResponseCreator.create(context, cacheInfo, bidderToMultiBid) - .map(bidResponse -> publishAuctionEvent(bidResponse, context)) - .map(bidResponse -> criteriaLogManager.traceResponse(logger, bidResponse, - context.getBidRequest(), context.getDebugContext().isDebugEnabled())) + .map(bidResponse -> criteriaLogManager.traceResponse( + logger, + bidResponse, + context.getBidRequest(), + context.getDebugContext().isDebugEnabled())) .compose(bidResponse -> bidResponsePostProcessor.postProcess( context.getHttpRequest(), uidsCookie, bidRequest, bidResponse, account)) .map(context::with)); } private BidderAliases aliases(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final Map aliases = prebid != null ? prebid.getAliases() : null; final Map aliasgvlids = prebid != null ? prebid.getAliasgvlids() : null; return BidderAliases.of(aliases, aliasgvlids, bidderCatalog); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); return prebid != null ? prebid.getTargeting() : null; } - /** - * Creates {@link BidRequestCacheInfo} based on {@link BidRequest} model. - */ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { final ExtRequestTargeting targeting = targeting(bidRequest); - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; if (targeting != null && cache != null) { @@ -387,17 +336,11 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { .build(); } } - return BidRequestCacheInfo.noCache(); } - private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { - final ExtRequest requestExt = bidRequest.getExt(); - return requestExt != null ? requestExt.getPrebid() : null; - } - private static Map bidderToMultiBids(BidRequest bidRequest, List debugWarnings) { - final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid extRequestPrebid = PbsUtil.extRequestPrebid(bidRequest); final Collection multiBids = extRequestPrebid != null ? CollectionUtils.emptyIfNull(extRequestPrebid.getMultibid()) : Collections.emptyList(); @@ -410,9 +353,8 @@ private static Map bidderToMultiBids(BidRequest bidReque final String codePrefix = prebidMultiBid.getTargetBidderCodePrefix(); if (bidder != null && CollectionUtils.isNotEmpty(bidders)) { - debugWarnings.add( - "Invalid MultiBid: bidder %s and bidders %s specified. Only bidder %s will be used." - .formatted(bidder, bidders, bidder)); + debugWarnings.add("Invalid MultiBid: bidder %s and bidders %s specified. Only bidder %s will be used." + .formatted(bidder, bidders, bidder)); tryAddBidderWithMultiBid(bidder, maxBids, codePrefix, bidderToMultiBid, debugWarnings); continue; } @@ -481,44 +423,12 @@ private Map makeBidRejectionTrackers(BidRequest bid entry -> new BidRejectionTracker(entry.getKey(), entry.getValue(), logSamplingRate))); } - /** - * Populates storedResponse parameter with stored {@link List} and returns {@link List} for which - * request to bidders should be performed. - */ private static StoredResponseResult populateStoredResponse(StoredResponseResult storedResponseResult, List storedResponse) { storedResponse.addAll(storedResponseResult.getAuctionStoredResponse()); return storedResponseResult; } - /** - * Takes an OpenRTB request and returns the OpenRTB requests sanitized for each bidder. - *

- * This will copy the {@link BidRequest} into a list of requests, where the bidRequest.imp[].ext field - * will only consist of the "prebid" field and the field for the appropriate bidder parameters. We will drop all - * extended fields beyond this context, so this will not be compatible with any other uses of the extension area - * i.e. the bidders will not see any other extension fields. If Imp extension name is alias, which is also defined - * in bidRequest.ext.prebid.aliases and valid, separate {@link BidRequest} will be created for this alias and sent - * to appropriate bidder. - * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext.prebid.bidder.rubicon and - * imp.ext.prebid.bidder.rubiconAlias and second with imp.ext.prebid.bidder.appnexus and - * imp.ext.prebid.bidder.rubicon. Three {@link BidRequest}s will be created: - * 1. {@link BidRequest} with one {@link Imp}, where bidder extension points to rubiconAlias extension and will be - * sent to Rubicon bidder. - * 2. {@link BidRequest} with two {@link Imp}s, where bidder extension points to appropriate rubicon extension from - * original {@link BidRequest} and will be sent to Rubicon bidder. - * 3. {@link BidRequest} with one {@link Imp}, where bidder extension points to appnexus extension and will be sent - * to Appnexus bidder. - *

- * Each of the created {@link BidRequest}s will have bidrequest.user.buyerid field populated with the value from - * bidrequest.user.ext.prebid.buyerids or {@link UidsCookie} corresponding to bidder's family name unless buyerid - * is already in the original OpenRTB request (in this case it will not be overridden). - * In case if bidrequest.user.ext.prebid.buyerids contains values after extracting those values it will be cleared - * in order to avoid leaking of buyerids across bidders. - *

- * NOTE: the return list will only contain entries for bidders that both have the extension field in at least one - * {@link Imp}, and are known to {@link BidderCatalog} or aliases from bidRequest.ext.prebid.aliases. - */ private Future> extractAuctionParticipations( AuctionContext context, StoredResponseResult storedResponseResult, @@ -537,9 +447,13 @@ private Future> extractAuctionParticipations( .toList(); final Map> impBidderToStoredBidResponse = storedResponseResult.getImpBidderToStoredBidResponse(); - - return makeAuctionParticipation(bidders, context, aliases, impBidderToStoredBidResponse, - imps, bidderToMultiBid); + return makeAuctionParticipation( + bidders, + context, + aliases, + impBidderToStoredBidResponse, + imps, + bidderToMultiBid); } private Set bidderNamesFromImpExt(Imp imp, BidderAliases aliases) { @@ -553,9 +467,6 @@ private static JsonNode bidderParamsFromImpExt(ObjectNode ext) { return ext.get(PREBID_EXT).get(BIDDER_EXT); } - /** - * Checks if bidder name is valid in case when bidder can also be alias name. - */ private boolean isValidBidder(String bidder, BidderAliases aliases) { return bidderCatalog.isValidName(bidder) || aliases.isAliasDefined(bidder); } @@ -571,21 +482,6 @@ private static boolean isBidderCallActivityAllowed(String bidder, AuctionContext activityInvocationPayload); } - /** - * Splits the input request into requests which are sanitized for each bidder. Intended behavior is: - *

- * - bidrequest.imp[].ext will only contain the "prebid" field and a "bidder" field which has the params for - * the intended Bidder. - *

- * - bidrequest.user.buyeruid will be set to that Bidder's ID. - *

- * - bidrequest.ext.prebid.data.bidders will be removed. - *

- * - bidrequest.ext.prebid.bidders will be staying in corresponding bidder only. - *

- * - bidrequest.user.ext.data, bidrequest.app.ext.data, bidrequest.dooh.ext.data and bidrequest.site.ext.data - * will be removed for bidders that don't have first party data allowed. - */ private Future> makeAuctionParticipation( List bidders, AuctionContext context, @@ -603,9 +499,15 @@ private Future> makeAuctionParticipation( prepareUsers(bidders, context, aliases, biddersToConfigs, eidPermissions); return privacyEnforcementService.mask(context, bidderToUser, aliases) - .map(bidderToPrivacyResult -> - getAuctionParticipation(bidderToPrivacyResult, bidRequest, impBidderToStoredResponse, imps, - bidderToMultiBid, biddersToConfigs, aliases, context)); + .map(bidderToPrivacyResult -> getAuctionParticipation( + bidderToPrivacyResult, + bidRequest, + impBidderToStoredResponse, + imps, + bidderToMultiBid, + biddersToConfigs, + aliases, + context)); } private Map getBiddersToConfigs(ExtRequestPrebid prebid) { @@ -632,10 +534,6 @@ private Map getBiddersToConfigs(ExtRequestPrebid pr return bidderToConfig; } - /** - * Retrieves user eids from {@link ExtRequestPrebid} and converts them to map, where keys are eids sources - * and values are allowed bidders - */ private Map> getEidPermissions(ExtRequestPrebid prebid) { final ExtRequestPrebidData prebidData = prebid != null ? prebid.getData() : null; final List eidPermissions = prebidData != null @@ -646,9 +544,6 @@ private Map> getEidPermissions(ExtRequestPrebid prebid) { ExtRequestPrebidDataEidPermissions::getBidders)); } - /** - * Extracts a list of bidders for which first party data is allowed from {@link ExtRequestPrebidData} model. - */ private static List firstPartyDataBidders(ExtRequest requestExt) { final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final ExtRequestPrebidData data = prebid == null ? null : prebid.getData(); @@ -677,13 +572,6 @@ private Map prepareUsers(List bidders, return bidderToUser; } - /** - * Returns original {@link User} if user.buyeruid already contains uid value for bidder. - * Otherwise, returns new {@link User} containing updated {@link ExtUser} and user.buyeruid. - *

- * Also, removes user.ext.prebid (if present), user.ext.data and user.data (in case bidder does not use first - * party data). - */ private User prepareUser(String bidder, AuctionContext context, BidderAliases aliases, @@ -708,7 +596,7 @@ private User prepareUser(String bidder, userBuilder.buyeruid(buyerUidUpdateResult.getValue()); if (shouldUpdateUserEids) { - userBuilder.eids(nullIfEmpty(allowedUserEids)); + userBuilder.eids(ListUtil.nullIfEmpty(allowedUserEids)); } if (shouldUpdateUserExt) { @@ -735,9 +623,6 @@ private List extractUserEids(User user) { return user != null ? user.getEids() : null; } - /** - * Returns {@link List} allowed by {@param eidPermissions} per source per bidder. - */ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions) { return CollectionUtils.emptyIfNull(userEids) .stream() @@ -745,10 +630,6 @@ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions, String bidder) { final List allowedBidders = eidPermissions.get(source); return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() @@ -756,9 +637,6 @@ private boolean isUserEidAllowed(String source, Map> eidPer || EID_ALLOWED_FOR_ALL_BIDDERS.equals(allowedBidder)); } - /** - * Returns shuffled list of {@link AuctionParticipation} with {@link BidRequest}. - */ private List getAuctionParticipation( List bidderPrivacyResults, BidRequest bidRequest, @@ -795,7 +673,7 @@ private List getAuctionParticipation( * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ private static Map bidderToPrebidBidders(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); if (bidders == null || bidders.isNull()) { @@ -811,9 +689,6 @@ private static Map bidderToPrebidBidders(BidRequest bidRequest return bidderToPrebidParameters; } - /** - * Returns {@link AuctionParticipation} for the given bidder. - */ private AuctionParticipation createAuctionParticipation( BidderPrivacyResult bidderPrivacyResult, Map> impBidderToStoredBidResponse, @@ -830,7 +705,7 @@ private AuctionParticipation createAuctionParticipation( if (blockedRequestByTcf) { context.getBidRejectionTrackers() .get(bidder) - .rejectAll(BidRejectionReason.REJECTED_BY_PRIVACY); + .rejectAll(BidRejectionReason.REQUEST_BLOCKED_PRIVACY); return AuctionParticipation.builder() .bidder(bidder) @@ -842,7 +717,7 @@ private AuctionParticipation createAuctionParticipation( final OrtbVersion ortbVersion = bidderSupportedOrtbVersion(bidder, bidderAliases); // stored bid response supported only for single imp requests final String storedBidResponse = impBidderToStoredBidResponse.size() == 1 - ? impBidderToStoredBidResponse.get(imps.get(0).getId()).get(bidder) + ? impBidderToStoredBidResponse.get(imps.getFirst().getId()).get(bidder) : null; final BidRequest preparedBidRequest = prepareBidRequest( bidderPrivacyResult, @@ -850,6 +725,7 @@ private AuctionParticipation createAuctionParticipation( bidderToMultiBid, biddersToConfigs, bidderToPrebidBidders, + bidderAliases, context); final BidderRequest bidderRequest = BidderRequest.builder() @@ -876,10 +752,16 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, Map bidderToMultiBid, Map biddersToConfigs, Map bidderToPrebidBidders, + BidderAliases bidderAliases, AuctionContext context) { - final BidRequest bidRequest = context.getBidRequest(); final String bidder = bidderPrivacyResult.getRequestBidder(); + final BidRequest bidRequest = priceFloorProcessor.enrichWithPriceFloors( + context.getBidRequest().toBuilder().imp(imps).build(), + context.getAccount(), + bidder, + context.getPrebidErrors(), + context.getDebugWarnings()); final boolean transmitTid = transmitTransactionId(bidder, context); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.stream() @@ -925,11 +807,20 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, final boolean isDooh = !isApp && preparedDooh != null; final boolean isSite = !isApp && !isDooh && preparedSite != null; + final List preparedImps = prepareImps( + bidder, + bidRequest, + transmitTid, + useFirstPartyData, + context.getAccount(), + bidderAliases, + context.getDebugWarnings()); + return bidRequest.toBuilder() // User was already prepared above .user(bidderPrivacyResult.getUser()) .device(bidderPrivacyResult.getDevice()) - .imp(prepareImps(bidder, imps, bidRequest, transmitTid, useFirstPartyData, context.getAccount())) + .imp(preparedImps) .app(isApp ? preparedApp : null) .dooh(isDooh ? preparedDooh : null) .site(isSite ? preparedSite : null) @@ -955,20 +846,19 @@ private static boolean transmitTransactionId(String bidder, AuctionContext conte return createTids; } - /** - * For each given imp creates a new imp with extension crafted to contain only "prebid", "context" and - * bidder-specific extension. - */ private List prepareImps(String bidder, - List imps, BidRequest bidRequest, boolean transmitTid, boolean useFirstPartyData, - Account account) { + Account account, + BidderAliases bidderAliases, + List debugWarnings) { - return imps.stream() + return bidRequest.getImp().stream() .filter(imp -> bidderParamsFromImpExt(imp.getExt()).hasNonNull(bidder)) - .map(imp -> prepareImp(imp, bidder, bidRequest, transmitTid, useFirstPartyData, account)) + .map(imp -> imp.toBuilder().ext(imp.getExt().deepCopy()).build()) + .map(imp -> impAdjuster.adjust(imp, bidder, bidderAliases, debugWarnings)) + .map(imp -> prepareImp(imp, bidder, bidRequest, transmitTid, useFirstPartyData, account, debugWarnings)) .toList(); } @@ -977,48 +867,42 @@ private Imp prepareImp(Imp imp, BidRequest bidRequest, boolean transmitTid, boolean useFirstPartyData, - Account account) { + Account account, + List debugWarnings) { - final BigDecimal adjustedFloor = resolveBidFloor(imp, bidder, bidRequest, account); + final Price adjustedPrice = resolveBidPrice(imp, bidder, bidRequest, account, debugWarnings); return imp.toBuilder() - .bidfloor(adjustedFloor) - .ext(prepareImpExt(bidder, imp.getExt(), adjustedFloor, transmitTid, useFirstPartyData)) + .bidfloor(adjustedPrice.getValue()) + .bidfloorcur(adjustedPrice.getCurrency()) + .ext(prepareImpExt(bidder, imp.getExt(), adjustedPrice.getValue(), transmitTid, useFirstPartyData)) .build(); } - /** - * @return Bidfloor divided by factor from {@link PriceFloorAdjuster} - */ - private BigDecimal resolveBidFloor(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return priceFloorAdjuster.adjustForImp(imp, bidder, bidRequest, account); + private Price resolveBidPrice(Imp imp, + String bidder, + BidRequest bidRequest, + Account account, + List debugWarnings) { + + return priceFloorAdjuster.adjustForImp(imp, bidder, bidRequest, account, debugWarnings); } - /** - * Creates a new imp extension for particular bidder having: - *

    - *
  • "prebid" field populated with an imp.ext.prebid field value, may be null
  • - *
  • "bidder" field populated with an imp.ext.prebid.bidder.{bidder} field value, not null
  • - *
  • "context" field populated with an imp.ext.context field value, may be null
  • - *
  • "data" field populated with an imp.ext.data field value, may be null
  • - *
- */ private ObjectNode prepareImpExt(String bidder, ObjectNode impExt, BigDecimal adjustedFloor, boolean transmitTid, boolean useFirstPartyData) { - - final ObjectNode modifiedImpExt = impExt.deepCopy(); + final JsonNode bidderNode = bidderParamsFromImpExt(impExt).get(bidder); final JsonNode impExtPrebid = prepareImpExt(impExt.get(PREBID_EXT), adjustedFloor); Optional.ofNullable(impExtPrebid).ifPresentOrElse( - ext -> modifiedImpExt.set(PREBID_EXT, ext), - () -> modifiedImpExt.remove(PREBID_EXT)); - modifiedImpExt.set(BIDDER_EXT, bidderParamsFromImpExt(impExt).get(bidder)); + ext -> impExt.set(PREBID_EXT, ext), + () -> impExt.remove(PREBID_EXT)); + impExt.set(BIDDER_EXT, bidderNode); if (!transmitTid) { - modifiedImpExt.remove(TID_EXT); + impExt.remove(TID_EXT); } - return fpdResolver.resolveImpExt(modifiedImpExt, useFirstPartyData); + return fpdResolver.resolveImpExt(impExt, useFirstPartyData); } private JsonNode prepareImpExt(JsonNode extImpPrebidNode, BigDecimal adjustedFloor) { @@ -1042,9 +926,6 @@ private JsonNode prepareImpExt(JsonNode extImpPrebidNode, BigDecimal adjustedFlo .build()); } - /** - * Returns {@link ExtImpPrebid} from imp.ext.prebid {@link JsonNode}. - */ private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { try { return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); @@ -1053,10 +934,6 @@ private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { } } - /** - * Checks whether to pass the app.ext.data and app.content.data depending on request having a first party data - * allowed for given bidder or not. And merge masked app with fpd config. - */ private App prepareApp(App app, ObjectNode fpdApp, boolean useFirstPartyData) { final ExtApp appExt = app != null ? app.getExt() : null; final Content content = app != null ? app.getContent() : null; @@ -1078,10 +955,6 @@ private static ExtApp maskExtApp(ExtApp appExt) { return maskedExtApp.isEmpty() ? null : maskedExtApp; } - /** - * Checks whether to pass the site.ext.data and site.content.data depending on request having a first party data - * allowed for given bidder or not. And merge masked site with fpd config. - */ private Site prepareSite(Site site, ObjectNode fpdSite, boolean useFirstPartyData) { final ExtSite siteExt = site != null ? site.getExt() : null; final Content content = site != null ? site.getContent() : null; @@ -1103,10 +976,6 @@ private static ExtSite maskExtSite(ExtSite siteExt) { return maskedExtSite.isEmpty() ? null : maskedExtSite; } - /** - * Checks whether to pass the dooh.ext.data and dooh.content.data depending on request having a first party data - * allowed for given bidder or not. And merge masked dooh with fpd config. - */ private Dooh prepareDooh(Dooh dooh, ObjectNode fpdDooh, boolean useFirstPartyData) { final ExtDooh doohExt = dooh != null ? dooh.getExt() : null; final Content content = dooh != null ? dooh.getContent() : null; @@ -1128,9 +997,6 @@ private static Content prepareContent(Content content) { return updatedContent.isEmpty() ? null : updatedContent; } - /** - * Returns {@link Source} with corresponding request.ext.prebid.schains. - */ private Source prepareSource(String bidder, BidRequest bidRequest, boolean transmitTid) { final Source receivedSource = bidRequest.getSource(); @@ -1148,12 +1014,6 @@ private Source prepareSource(String bidder, BidRequest bidRequest, boolean trans .build(); } - /** - * Removes all bidders except the given bidder from bidrequest.ext.prebid.bidders to hide list of allowed bidders - * from initial request. - *

- * Also masks bidrequest.ext.prebid.schains. - */ private ExtRequest prepareExt(String bidder, Map bidderToPrebidBidders, Map bidderToMultiBid, @@ -1209,45 +1069,12 @@ private List resolveExtRequestMultiBids(MultiBidConfig : null; } - /** - * Prepares parameters for specified bidder removing parameters for all other bidders. - * Returns null if there are no parameters for specified bidder. - */ private ObjectNode prepareBidderParameters(ExtRequestPrebid prebid, String bidder) { final ObjectNode bidderParams = prebid != null ? prebid.getBidderparams() : null; final JsonNode params = bidderParams != null ? bidderParams.get(bidder) : null; return params != null ? mapper.mapper().createObjectNode().set(bidder, params) : null; } - private List matchAndPopulateDeals(List auctionParticipants, - BidderAliases aliases, - AuctionContext context) { - - if (dealsService == null) { - return auctionParticipants; - } - - final List updatedBidderRequests = auctionParticipants.stream() - .map(auctionParticipation -> !auctionParticipation.isRequestBlocked() - ? dealsService.matchAndPopulateDeals(auctionParticipation.getBidderRequest(), aliases, context) - : null) - .toList(); - - return IntStream.range(0, auctionParticipants.size()) - .mapToObj(i -> auctionParticipants.get(i).toBuilder() - .bidderRequest(updatedBidderRequests.get(i)) - .build()) - .toList(); - } - - private static List postProcessDeals(List auctionParticipations, - AuctionContext context) { - return DealsService.removePgDealsOnlyImpsWithoutDeals(auctionParticipations, context); - } - - /** - * Updates 'account.*.request', 'request' and 'no_cookie_requests' metrics for each {@link AuctionParticipation} . - */ private AuctionContext updateRequestMetric(AuctionContext context, UidsCookie uidsCookie, BidderAliases aliases, @@ -1278,26 +1105,6 @@ private AuctionContext updateRequestMetric(AuctionContext context, return context; } - private static AuctionContext fillContext(AuctionContext context, - List auctionParticipations) { - - final Map> impIdToDeals = new HashMap<>(); - auctionParticipations.stream() - .map(AuctionParticipation::getBidderRequest) - .map(BidderRequest::getImpIdToDeals) - .filter(Objects::nonNull) - .map(Map::entrySet) - .flatMap(Collection::stream) - .forEach(entry -> impIdToDeals - .computeIfAbsent(entry.getKey(), key -> new ArrayList<>()) - .addAll(entry.getValue())); - - return context.toBuilder() - .bidRequest(DealsService.populateDeals(context.getBidRequest(), impIdToDeals)) - .auctionParticipations(auctionParticipations) - .build(); - } - private Future processAndRequestBids(AuctionContext auctionContext, BidderRequest bidderRequest, Timeout timeout, @@ -1306,16 +1113,20 @@ private Future processAndRequestBids(AuctionContext auctionConte final String bidderName = bidderRequest.getBidder(); final MediaTypeProcessingResult mediaTypeProcessingResult = mediaTypeProcessor.process( bidderRequest.getBidRequest(), bidderName, aliases, auctionContext.getAccount()); - final List mediaTypeProcessingErrors = mediaTypeProcessingResult.getErrors(); if (mediaTypeProcessingResult.isRejected()) { - auctionContext.getBidRejectionTrackers() - .get(bidderName) - .rejectAll(BidRejectionReason.REJECTED_BY_MEDIA_TYPE); - final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() - .warnings(mediaTypeProcessingErrors) - .build(); - return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, + mediaTypeProcessingErrors, + bidderName); + } + if (isUnacceptableCurrency(auctionContext, aliases.resolveBidder(bidderName))) { + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, + List.of(BidderError.generic("No match between the configured currencies and bidRequest.cur")), + bidderName); } return Future.succeededFuture(mediaTypeProcessingResult.getBidRequest()) @@ -1326,6 +1137,34 @@ private Future processAndRequestBids(AuctionContext auctionConte addWarnings(bidderResponse.getSeatBid(), mediaTypeProcessingErrors))); } + private boolean isUnacceptableCurrency(AuctionContext auctionContext, String originalBidderName) { + final List requestCurrencies = auctionContext.getBidRequest().getCur(); + final List bidAcceptableCurrencies = + Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) + .map(BidderInfo::getCurrencyAccepted) + .orElse(null); + + if (CollectionUtils.isEmpty(requestCurrencies) || CollectionUtils.isEmpty(bidAcceptableCurrencies)) { + return false; + } + + return !CollectionUtils.containsAny(requestCurrencies, bidAcceptableCurrencies); + } + + private static Future processReject(AuctionContext auctionContext, + BidRejectionReason bidRejectionReason, + List warnings, + String bidderName) { + + auctionContext.getBidRejectionTrackers() + .get(bidderName) + .rejectAll(bidRejectionReason); + final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() + .warnings(warnings) + .build(); + return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + } + private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { return CollectionUtils.isNotEmpty(warnings) ? seatBid.toBuilder() @@ -1358,7 +1197,7 @@ private Future requestBidsOrRejectBidder( if (hookStageResult.isShouldReject()) { auctionContext.getBidRejectionTrackers() .get(bidderRequest.getBidder()) - .rejectAll(BidRejectionReason.REJECTED_BY_HOOK); + .rejectAll(BidRejectionReason.REQUEST_BLOCKED_GENERAL); return Future.succeededFuture(BidderResponse.of(bidderRequest.getBidder(), BidderSeatBid.empty(), 0)); } @@ -1466,233 +1305,10 @@ private boolean isZeroNonDealBids(BigDecimal price, String dealId) { || (price.compareTo(BigDecimal.ZERO) == 0 && StringUtils.isBlank(dealId)); } - private List validateAndAdjustBids(List auctionParticipations, - AuctionContext auctionContext, - BidderAliases aliases) { - - return auctionParticipations.stream() - .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) - .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) - .map(auctionParticipation -> priceFloorEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getAccount(), - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .map(auctionParticipation -> dsaEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .toList(); - } - - /** - * Validates bid response from exchange. - *

- * Removes invalid bids from response and adds corresponding error to {@link BidderSeatBid}. - *

- * Returns input argument as the result if no errors found or creates new {@link BidderResponse} otherwise. - */ - private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, - AuctionContext auctionContext, - BidderAliases aliases) { - - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidRequest bidRequest = auctionContext.getBidRequest(); - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final List errors = new ArrayList<>(seatBid.getErrors()); - final List warnings = new ArrayList<>(seatBid.getWarnings()); - - final List requestCurrencies = bidRequest.getCur(); - if (requestCurrencies.size() > 1) { - errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" - .formatted(requestCurrencies.get(0)))); - } - - final List bids = seatBid.getBids(); - final List validBids = new ArrayList<>(bids.size()); - final TxnLog txnLog = auctionContext.getTxnLog(); - final String bidder = bidderResponse.getBidder(); - - for (final BidderBid bid : bids) { - final String lineItemId = LineItemUtil.lineItemIdFrom(bid.getBid(), bidRequest.getImp(), mapper); - maybeRecordInTxnLog(lineItemId, () -> txnLog.lineItemsReceivedFromBidder().get(bidder)); - - final ValidationResult validationResult = responseBidValidator.validate( - bid, - bidderResponse.getBidder(), - auctionContext, - aliases); - - if (validationResult.hasWarnings() || validationResult.hasErrors()) { - errors.add(makeValidationBidderError(bid.getBid(), validationResult)); - } - - if (validationResult.hasErrors()) { - maybeRecordInTxnLog(lineItemId, txnLog::lineItemsResponseInvalidated); - continue; - } - - if (!validationResult.hasErrors()) { - validBids.add(bid); - } - } - - final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() - ? bidderResponse - : bidderResponse.with( - seatBid.toBuilder() - .bids(validBids) - .errors(errors) - .warnings(warnings) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { - final String validationErrors = Stream.concat( - validationResult.getErrors().stream().map(message -> "Error: " + message), - validationResult.getWarnings().stream().map(message -> "Warning: " + message)) - .collect(Collectors.joining(". ")); - - final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); - return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); - } - - private static void maybeRecordInTxnLog(String lineItemId, Supplier> metricSupplier) { - if (lineItemId != null) { - metricSupplier.get().add(lineItemId); - } - } - - private BidResponse publishAuctionEvent(BidResponse bidResponse, AuctionContext auctionContext) { - if (applicationEventService != null) { - applicationEventService.publishAuctionEvent(auctionContext); - } - return bidResponse; - } - - /** - * Performs changes on {@link Bid}s price depends on different between adServerCurrency and bidCurrency, - * and adjustment factor. Will drop bid if currency conversion is needed but not possible. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ - private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, - BidRequest bidRequest) { - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - - final List bidderBids = seatBid.getBids(); - if (bidderBids.isEmpty()) { - return auctionParticipation; - } - - final List updatedBidderBids = new ArrayList<>(bidderBids.size()); - final List errors = new ArrayList<>(seatBid.getErrors()); - final String adServerCurrency = bidRequest.getCur().get(0); - - for (final BidderBid bidderBid : bidderBids) { - try { - final BidderBid updatedBidderBid = - updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); - updatedBidderBids.add(updatedBidderBid); - } catch (PreBidException e) { - errors.add(BidderError.generic(e.getMessage())); - } - } - - final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() - .bids(updatedBidderBids) - .errors(errors) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, - BidderResponse bidderResponse, - BidRequest bidRequest, - String adServerCurrency) { - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidderBid.getBidCurrency(); - final BigDecimal price = bid.getPrice(); - - final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); - - final BigDecimal priceAdjustmentFactor = - bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); - final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); - - final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); - - updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); - - final Bid.BidBuilder bidBuilder = bid.toBuilder(); - if (adjustedPrice.compareTo(price) != 0) { - bidBuilder.price(adjustedPrice); - } - - if (!updatedBidExt.isEmpty()) { - bidBuilder.ext(updatedBidExt); - } - - return bidderBid.toBuilder().bid(bidBuilder.build()).build(); - } - - private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { - final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); - - return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); - } - - private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - return prebid != null ? prebid.getBidadjustmentfactors() : null; - } - - private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { - return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 - ? price.multiply(priceAdjustmentFactor) - : price; - } - - private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); - if (StringUtils.isNotBlank(bidCurrency)) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); - } - } - - private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { - node.set(propertyName, propertyValue); - } - private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } - /** - * Updates 'request_time', 'responseTime', 'timeout_request', 'error_requests', 'no_bid_requests', - * 'prices' metrics for each {@link AuctionParticipation}. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ private List updateResponsesMetrics(List auctionParticipations, Account account, BidderAliases aliases) { @@ -1716,8 +1332,8 @@ private List updateResponsesMetrics(List invokeResponseHooks(AuctionContext auctionContext .map(auctionContext::with); } - /** - * Resolves {@link MetricName} by {@link BidderError.Type} value. - */ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { return switch (errorType) { case bad_input -> MetricName.badinput; @@ -1751,201 +1364,10 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { case failed_to_request_bids -> MetricName.failedtorequestbids; case timeout -> MetricName.timeout; case invalid_bid -> MetricName.bid_validation; - case rejected_ipf, generic, invalid_creative -> MetricName.unknown_error; + case rejected_ipf, generic -> MetricName.unknown_error; }; } - private AuctionContext enrichWithHooksDebugInfo(AuctionContext context) { - final ExtModules extModules = toExtModules(context); - - if (extModules == null) { - return context; - } - - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); - - final ExtBidResponsePrebid updatedExtPrebid = extPrebid - .map(ExtBidResponsePrebid::toBuilder) - .orElse(ExtBidResponsePrebid.builder()) - .modules(extModules) - .build(); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .prebid(updatedExtPrebid) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private static ExtModules toExtModules(AuctionContext context) { - final Map>> errors = - toHookMessages(context, HookExecutionOutcome::getErrors); - final Map>> warnings = - toHookMessages(context, HookExecutionOutcome::getWarnings); - final ExtModulesTrace trace = toHookTrace(context); - return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; - } - - private static Map>> toHookMessages( - AuctionContext context, - Function> messagesGetter) { - - if (!context.getDebugContext().isDebugEnabled()) { - return null; - } - - final Map> hookOutcomesByModule = - context.getHookExecutionContext().getStageOutcomes().values().stream() - .flatMap(Collection::stream) - .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) - .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) - .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) - .collect(Collectors.groupingBy( - hookOutcome -> hookOutcome.getHookId().getModuleCode())); - - final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - outcomes -> outcomes.getValue().stream() - .collect(Collectors.groupingBy( - hookOutcome -> hookOutcome.getHookId().getHookImplCode())) - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - messagesLists -> messagesLists.getValue().stream() - .map(messagesGetter) - .flatMap(Collection::stream) - .toList())))); - - return !messagesByModule.isEmpty() ? messagesByModule : null; - } - - private static ExtModulesTrace toHookTrace(AuctionContext context) { - final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); - - if (traceLevel == null) { - return null; - } - - final List stages = context.getHookExecutionContext().getStageOutcomes() - .entrySet().stream() - .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) - .filter(Objects::nonNull) - .toList(); - - if (stages.isEmpty()) { - return null; - } - - final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); - return ExtModulesTrace.of(executionTime, stages); - } - - private static ExtModulesTraceStage toTraceStage(Stage stage, - List stageOutcomes, - TraceLevel level) { - - final List extStageOutcomes = stageOutcomes.stream() - .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) - .filter(Objects::nonNull) - .toList(); - - if (extStageOutcomes.isEmpty()) { - return null; - } - - final long executionTime = extStageOutcomes.stream() - .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) - .max() - .orElse(0L); - - return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); - } - - private static ExtModulesTraceStageOutcome toTraceStageOutcome( - StageExecutionOutcome stageOutcome, TraceLevel level) { - - final List groups = stageOutcome.getGroups().stream() - .map(group -> toTraceGroup(group, level)) - .toList(); - - if (groups.isEmpty()) { - return null; - } - - final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); - return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); - } - - private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { - final List invocationResults = group.getHooks().stream() - .map(hook -> toTraceInvocationResult(hook, level)) - .toList(); - - final long executionTime = invocationResults.stream() - .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) - .max() - .orElse(0L); - - return ExtModulesTraceGroup.of(executionTime, invocationResults); - } - - private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, - TraceLevel level) { - return ExtModulesTraceInvocationResult.builder() - .hookId(hook.getHookId()) - .executionTime(hook.getExecutionTime()) - .status(hook.getStatus()) - .message(hook.getMessage()) - .action(hook.getAction()) - .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) - .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) - .build(); - } - - private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { - if (analyticsTags == null) { - return null; - } - - return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() - .filter(Objects::nonNull) - .map(ExchangeService::toTraceAnalyticsActivity) - .toList()); - } - - private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity( - org.prebid.server.hooks.v1.analytics.Activity activity) { - - return ExtModulesTraceAnalyticsActivity.of( - activity.name(), - activity.status(), - CollectionUtils.emptyIfNull(activity.results()).stream() - .filter(Objects::nonNull) - .map(ExchangeService::toTraceAnalyticsResult) - .toList()); - } - - private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { - final AppliedTo appliedTo = result.appliedTo(); - final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null - ? ExtModulesTraceAnalyticsAppliedTo.builder() - .impIds(appliedTo.impIds()) - .bidders(appliedTo.bidders()) - .request(appliedTo.request() ? Boolean.TRUE : null) - .response(appliedTo.response() ? Boolean.TRUE : null) - .bidIds(appliedTo.bidIds()) - .build() - : null; - - return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); - } - private AuctionContext updateHooksMetrics(AuctionContext context) { final EnumMap> stageOutcomes = context.getHookExecutionContext().getStageOutcomes(); @@ -1998,8 +1420,4 @@ private void updateHookInvocationMetrics(Account account, Stage stage, HookExecu metrics.updateAccountHooksMetrics(account, moduleCode, status, action); } } - - private List nullIfEmpty(List value) { - return CollectionUtils.isEmpty(value) ? null : value; - } } diff --git a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java new file mode 100644 index 00000000000..4c1a3b0b8cf --- /dev/null +++ b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java @@ -0,0 +1,98 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import io.vertx.core.Future; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; +import org.prebid.server.execution.Timeout; +import org.prebid.server.geolocation.GeoLocationService; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.Metrics; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountSettings; + +import java.util.Objects; +import java.util.Optional; + +public class GeoLocationServiceWrapper { + + private static final Logger logger = LoggerFactory.getLogger(GeoLocationServiceWrapper.class); + + private final GeoLocationService geoLocationService; + private final Ortb2ImplicitParametersResolver implicitParametersResolver; + private final Metrics metrics; + + public GeoLocationServiceWrapper(GeoLocationService geoLocationService, + Ortb2ImplicitParametersResolver implicitParametersResolver, + Metrics metrics) { + + this.geoLocationService = geoLocationService; + this.implicitParametersResolver = Objects.requireNonNull(implicitParametersResolver); + this.metrics = Objects.requireNonNull(metrics); + } + + public Future lookup(AuctionContext auctionContext) { + final Account account = auctionContext.getAccount(); + final Device device = auctionContext.getBidRequest().getDevice(); + final HttpRequestContext requestContext = auctionContext.getHttpRequest(); + final Timeout timeout = auctionContext.getTimeoutContext().getTimeout(); + + final boolean isGeoLookupEnabled = Optional.ofNullable(account.getSettings()) + .map(AccountSettings::getGeoLookup) + .map(BooleanUtils::isTrue) + .orElse(false); + + return isGeoLookupEnabled + ? doLookup(getIpAddress(device, requestContext), getCountry(device), timeout).otherwiseEmpty() + : Future.succeededFuture(); + } + + public Future doLookup(String ipAddress, String requestCountry, Timeout timeout) { + if (geoLocationService == null || ipAddress == null || StringUtils.isNotBlank(requestCountry)) { + return Future.failedFuture("Geolocation lookup is skipped"); + } + return geoLocationService.lookup(ipAddress, timeout) + .onSuccess(geoInfo -> metrics.updateGeoLocationMetric(true)) + .onFailure(this::logError); + } + + private String getCountry(Device device) { + return Optional.ofNullable(device) + .map(Device::getGeo) + .map(Geo::getCountry) + .filter(StringUtils::isNotBlank) + .orElse(null); + } + + private String getIpAddress(Device device, HttpRequestContext request) { + final Optional optionalDevice = Optional.ofNullable(device); + return optionalDevice.map(Device::getIp) + .filter(StringUtils::isNotBlank) + .or(() -> optionalDevice + .map(Device::getIpv6) + .filter(StringUtils::isNotBlank)) + .or(() -> ipFromHeader(request)) + .orElse(null); + } + + private Optional ipFromHeader(HttpRequestContext request) { + final IpAddress headerIp = implicitParametersResolver.findIpFromRequest(request); + return Optional.ofNullable(headerIp) + .map(IpAddress::getIp); + } + + private void logError(Throwable error) { + final String message = "Geolocation lookup failed: " + error.getMessage(); + logger.warn(message); + logger.debug(message, error); + + metrics.updateGeoLocationMetric(false); + } +} diff --git a/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java new file mode 100644 index 00000000000..ad8cd86410c --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java @@ -0,0 +1,247 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; + +import java.util.Collection; +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.Collectors; + +public class HookDebugInfoEnricher { + + private HookDebugInfoEnricher() { + } + + public static AuctionContext enrichWithHooksDebugInfo(AuctionContext context) { + final ExtModules extModules = toExtModules(context); + + if (extModules == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .modules(extModules) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static ExtModules toExtModules(AuctionContext context) { + final Map>> errors = + toHookMessages(context, HookExecutionOutcome::getErrors); + final Map>> warnings = + toHookMessages(context, HookExecutionOutcome::getWarnings); + final ExtModulesTrace trace = toHookTrace(context); + return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; + } + + private static Map>> toHookMessages( + AuctionContext context, + Function> messagesGetter) { + + if (!context.getDebugContext().isDebugEnabled()) { + return null; + } + + final Map> hookOutcomesByModule = + context.getHookExecutionContext().getStageOutcomes().values().stream() + .flatMap(Collection::stream) + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getModuleCode())); + + final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + outcomes -> outcomes.getValue().stream() + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getHookImplCode())) + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + messagesLists -> messagesLists.getValue().stream() + .map(messagesGetter) + .flatMap(Collection::stream) + .toList())))); + + return !messagesByModule.isEmpty() ? messagesByModule : null; + } + + private static ExtModulesTrace toHookTrace(AuctionContext context) { + final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); + + if (traceLevel == null) { + return null; + } + + final List stages = context.getHookExecutionContext().getStageOutcomes() + .entrySet().stream() + .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) + .filter(Objects::nonNull) + .toList(); + + if (stages.isEmpty()) { + return null; + } + + final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); + return ExtModulesTrace.of(executionTime, stages); + } + + private static ExtModulesTraceStage toTraceStage(Stage stage, + List stageOutcomes, + TraceLevel level) { + + final List extStageOutcomes = stageOutcomes.stream() + .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) + .filter(Objects::nonNull) + .toList(); + + if (extStageOutcomes.isEmpty()) { + return null; + } + + final long executionTime = extStageOutcomes.stream() + .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); + } + + private static ExtModulesTraceStageOutcome toTraceStageOutcome( + StageExecutionOutcome stageOutcome, TraceLevel level) { + + final List groups = stageOutcome.getGroups().stream() + .map(group -> toTraceGroup(group, level)) + .toList(); + + if (groups.isEmpty()) { + return null; + } + + final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); + return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); + } + + private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { + final List invocationResults = group.getHooks().stream() + .map(hook -> toTraceInvocationResult(hook, level)) + .toList(); + + final long executionTime = invocationResults.stream() + .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceGroup.of(executionTime, invocationResults); + } + + private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, + TraceLevel level) { + return ExtModulesTraceInvocationResult.builder() + .hookId(hook.getHookId()) + .executionTime(hook.getExecutionTime()) + .status(hook.getStatus()) + .message(hook.getMessage()) + .action(hook.getAction()) + .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) + .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) + .build(); + } + + private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { + if (analyticsTags == null) { + return null; + } + + return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() + .filter(Objects::nonNull) + .map(HookDebugInfoEnricher::toTraceAnalyticsActivity) + .toList()); + } + + private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity( + org.prebid.server.hooks.v1.analytics.Activity activity) { + + return ExtModulesTraceAnalyticsActivity.of( + activity.name(), + activity.status(), + CollectionUtils.emptyIfNull(activity.results()).stream() + .filter(Objects::nonNull) + .map(HookDebugInfoEnricher::toTraceAnalyticsResult) + .toList()); + } + + private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { + final AppliedTo appliedTo = result.appliedTo(); + final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null + ? ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(appliedTo.impIds()) + .bidders(appliedTo.bidders()) + .request(appliedTo.request() ? Boolean.TRUE : null) + .response(appliedTo.response() ? Boolean.TRUE : null) + .bidIds(appliedTo.bidIds()) + .build() + : null; + + return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); + } + + public static List toExtAnalyticsTags(AuctionContext context) { + return context.getHookExecutionContext().getStageOutcomes().entrySet().stream() + .flatMap(stageToExecutionOutcome -> stageToExecutionOutcome.getValue().stream() + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookExecutionOutcome -> hookExecutionOutcome.getAnalyticsTags() != null) + .map(hookExecutionOutcome -> ExtAnalyticsTags.of( + stageToExecutionOutcome.getKey(), + hookExecutionOutcome.getHookId().getModuleCode(), + toTraceAnalyticsTags(hookExecutionOutcome.getAnalyticsTags())))) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/auction/ImpAdjuster.java b/src/main/java/org/prebid/server/auction/ImpAdjuster.java new file mode 100644 index 00000000000..86e581b06e8 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/ImpAdjuster.java @@ -0,0 +1,98 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.validation.ImpValidator; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class ImpAdjuster { + + private static final String IMP_EXT = "ext"; + private static final String EXT_PREBID = "prebid"; + private static final String EXT_PREBID_BIDDER = "bidder"; + private static final String EXT_PREBID_IMP = "imp"; + + private final ImpValidator impValidator; + private final JacksonMapper jacksonMapper; + private final JsonMerger jsonMerger; + + public ImpAdjuster(JacksonMapper jacksonMapper, + JsonMerger jsonMerger, + ImpValidator impValidator) { + + this.impValidator = Objects.requireNonNull(impValidator); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public Imp adjust(Imp originalImp, String bidder, BidderAliases bidderAliases, List debugMessages) { + final JsonNode impExtPrebidImp = bidderParamsFromImpExtPrebidImp(originalImp.getExt()); + if (impExtPrebidImp == null) { + return originalImp; + } + + final JsonNode bidderNode = getBidderNode(bidder, bidderAliases, impExtPrebidImp); + + if (bidderNode == null || bidderNode.isEmpty()) { + removeImpExtPrebidImp(originalImp.getExt()); + return originalImp; + } + + removeExtPrebidBidder(bidderNode); + + try { + final JsonNode originalImpNode = jacksonMapper.mapper().valueToTree(originalImp); + final JsonNode mergedImpNode = jsonMerger.merge(bidderNode, originalImpNode); + + removeImpExtPrebidImp(mergedImpNode.get(IMP_EXT)); + + final Imp resultImp = jacksonMapper.mapper().convertValue(mergedImpNode, Imp.class); + + impValidator.validateImp(resultImp); + return resultImp; + } catch (Exception e) { + debugMessages.add("imp.ext.prebid.imp.%s can not be merged into original imp [id=%s], reason: %s" + .formatted(bidder, originalImp.getId(), e.getMessage())); + removeImpExtPrebidImp(originalImp.getExt()); + return originalImp; + } + } + + private static JsonNode bidderParamsFromImpExtPrebidImp(ObjectNode ext) { + return Optional.ofNullable(ext) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(prebidNode -> prebidNode.get(EXT_PREBID_IMP)) + .orElse(null); + } + + private static JsonNode getBidderNode(String bidderName, BidderAliases bidderAliases, JsonNode node) { + final Iterator fieldNames = node.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (bidderAliases.isSame(fieldName, bidderName)) { + return node.get(fieldName); + } + } + return null; + } + + private static void removeExtPrebidBidder(JsonNode bidderNode) { + Optional.ofNullable(bidderNode.get(IMP_EXT)) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .ifPresent(ext -> ext.remove(EXT_PREBID_BIDDER)); + } + + private static void removeImpExtPrebidImp(JsonNode impExt) { + Optional.ofNullable(impExt.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .ifPresent(prebid -> prebid.remove(EXT_PREBID_IMP)); + } +} diff --git a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java index dd523aff4f6..af31064f99d 100644 --- a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java +++ b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java @@ -55,7 +55,7 @@ private Imp processInterstitialImp(Imp imp, Device device, int minWidthPerc, int } final List formats = banner.getFormat(); - final Format firstFormat = CollectionUtils.isEmpty(formats) ? null : formats.get(0); + final Format firstFormat = CollectionUtils.isEmpty(formats) ? null : formats.getFirst(); Integer maxHeight = firstFormat != null ? firstFormat.getH() : null; Integer maxWidth = firstFormat != null ? firstFormat.getW() : null; diff --git a/src/main/java/org/prebid/server/auction/IpAddressHelper.java b/src/main/java/org/prebid/server/auction/IpAddressHelper.java index f99e75cb985..523219fd511 100644 --- a/src/main/java/org/prebid/server/auction/IpAddressHelper.java +++ b/src/main/java/org/prebid/server/auction/IpAddressHelper.java @@ -4,11 +4,11 @@ import inet.ipaddr.IPAddress; import inet.ipaddr.IPAddressString; import inet.ipaddr.IPAddressStringParameters; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.apache.http.conn.util.InetAddressUtils; import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import java.util.List; @@ -43,7 +43,7 @@ public String anonymizeIpv6(String ip) { ? ipAddressString.toAddress().mask(ipv6AnonLeftMaskAddress).toCanonicalString() : null; } catch (AddressStringException e) { - logger.debug("Exception occurred while anonymizing IPv6 address: {0}", e.getMessage()); + logger.debug("Exception occurred while anonymizing IPv6 address: {}", e.getMessage()); return null; } } diff --git a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java index 3a0063f66ea..35668bbf0f4 100644 --- a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java +++ b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java @@ -6,14 +6,14 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/org/prebid/server/auction/PriceGranularity.java b/src/main/java/org/prebid/server/auction/PriceGranularity.java index 81cec03fd62..75620e4d953 100644 --- a/src/main/java/org/prebid/server/auction/PriceGranularity.java +++ b/src/main/java/org/prebid/server/auction/PriceGranularity.java @@ -72,6 +72,12 @@ public static PriceGranularity createFromString(String stringPriceGranularity) { } } + public static PriceGranularity createFromStringOrDefault(String stringPriceGranularity) { + return isValidStringPriceGranularityType(stringPriceGranularity) + ? STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.valueOf(stringPriceGranularity)) + : PriceGranularity.DEFAULT; + } + /** * Returns list of {@link ExtGranularityRange}s. */ diff --git a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java new file mode 100644 index 00000000000..dd8c95c0f50 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java @@ -0,0 +1,103 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.StoredResponseResult; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class SkippedAuctionService { + + private final StoredResponseProcessor storedResponseProcessor; + private final BidResponseCreator bidResponseCreator; + + public SkippedAuctionService(StoredResponseProcessor storedResponseProcessor, + BidResponseCreator bidResponseCreator) { + + this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); + this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator); + } + + public Future skipAuction(AuctionContext auctionContext) { + if (auctionContext.isRequestRejected()) { + return Future.failedFuture("Rejected request cannot be skipped"); + } + + final ExtStoredAuctionResponse storedResponse = Optional.ofNullable(auctionContext.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getStoredAuctionResponse) + .orElse(null); + + if (storedResponse == null) { + return Future.failedFuture(new InvalidRequestException( + "the auction can not be skipped, ext.prebid.storedauctionresponse is absent")); + } + + final List seatBids = storedResponse.getSeatBids(); + if (seatBids != null) { + return validateStoredSeatBid(seatBids) + .recover(throwable -> { + auctionContext.getDebugWarnings().add(throwable.getMessage()); + return Future.succeededFuture(Collections.emptyList()); + }) + .compose(storedSeatBids -> enrichAuctionContextWithBidResponse(auctionContext, storedSeatBids)) + .map(AuctionContext::skipAuction); + } + + if (storedResponse.getId() != null) { + final Timeout timeout = auctionContext.getTimeoutContext().getTimeout(); + return storedResponseProcessor.getStoredResponseResult(storedResponse.getId(), timeout) + .map(StoredResponseResult::getAuctionStoredResponse) + .recover(throwable -> { + auctionContext.getDebugWarnings().add(throwable.getMessage()); + return Future.succeededFuture(Collections.emptyList()); + }) + .compose(storedSeatBids -> enrichAuctionContextWithBidResponse(auctionContext, storedSeatBids)) + .map(AuctionContext::skipAuction); + } + + return Future.failedFuture(new InvalidRequestException( + "the auction can not be skipped, ext.prebid.storedauctionresponse can not be resolved properly")); + + } + + private Future> validateStoredSeatBid(List seatBids) { + for (final SeatBid seatBid : seatBids) { + if (seatBid == null) { + return Future.failedFuture( + new InvalidRequestException("SeatBid can't be null in stored response")); + } + if (StringUtils.isEmpty(seatBid.getSeat())) { + return Future.failedFuture( + new InvalidRequestException("Seat can't be empty in stored response seatBid")); + } + + if (CollectionUtils.isEmpty(seatBid.getBid())) { + return Future.failedFuture( + new InvalidRequestException("There must be at least one bid in stored response seatBid")); + } + } + + return Future.succeededFuture(seatBids); + } + + private Future enrichAuctionContextWithBidResponse(AuctionContext auctionContext, + List seatBids) { + + auctionContext.getDebugWarnings().add("no auction. response defined by storedauctionresponse"); + return bidResponseCreator.createOnSkippedAuction(auctionContext, seatBids).map(auctionContext::with); + } +} diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java index 466c1197527..b769d2974b1 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java @@ -12,6 +12,7 @@ import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.StoredResponseResult; @@ -38,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -67,179 +69,176 @@ public StoredResponseProcessor(ApplicationSettings applicationSettings, Future getStoredResponseResult(List imps, Timeout timeout) { final Map impExtPrebids = getImpsExtPrebid(imps); - final Map auctionStoredResponseToImpId = getAuctionStoredResponses(impExtPrebids); - final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, auctionStoredResponseToImpId); + final Map impIdsToStoredResponses = getAuctionStoredResponses(impExtPrebids); + final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, impIdsToStoredResponses); - final Map> impToBidderToStoredBidResponseId = getStoredBidResponses(impExtPrebids, - requiredRequestImps); + final Map> impToBidderToStoredBidResponseId = + getStoredBidResponses(impExtPrebids, requiredRequestImps); - final Set storedIds = new HashSet<>(auctionStoredResponseToImpId.keySet()); + final Set storedResponses = new HashSet<>(impIdsToStoredResponses.values()); - storedIds.addAll( - impToBidderToStoredBidResponseId.values().stream() - .flatMap(bidderToId -> bidderToId.values().stream()) - .collect(Collectors.toSet())); + impToBidderToStoredBidResponseId.values() + .forEach(bidderToStoredResponse -> storedResponses.addAll(bidderToStoredResponse.values())); - if (storedIds.isEmpty()) { - return Future.succeededFuture(StoredResponseResult.of(imps, Collections.emptyList(), - Collections.emptyMap())); + if (storedResponses.isEmpty()) { + return Future.succeededFuture( + StoredResponseResult.of(imps, Collections.emptyList(), Collections.emptyMap())); } - return applicationSettings.getStoredResponses(storedIds, timeout) + return getStoredResponses(storedResponses, timeout) .recover(exception -> Future.failedFuture(new InvalidRequestException( "Stored response fetching failed with reason: " + exception.getMessage()))) .map(storedResponseDataResult -> StoredResponseResult.of( requiredRequestImps, - convertToSeatBid(storedResponseDataResult, auctionStoredResponseToImpId), - mapStoredBidResponseIdsToValues(storedResponseDataResult.getIdToStoredResponses(), + convertToSeatBid(storedResponseDataResult, impIdsToStoredResponses), + mapStoredBidResponseIdsToValues( + storedResponseDataResult.getIdToStoredResponses(), impToBidderToStoredBidResponseId))); } - private List excludeStoredAuctionResponseImps(List imps, - Map auctionStoredResponseToImpId) { - - return imps.stream() - .filter(imp -> !auctionStoredResponseToImpId.containsValue(imp.getId())) - .toList(); + Future getStoredResponseResult(String storedId, Timeout timeout) { + return applicationSettings.getStoredResponses(Collections.singleton(storedId), timeout) + .recover(exception -> Future.failedFuture(new InvalidRequestException( + "Stored response fetching failed with reason: " + exception.getMessage()))) + .map(storedResponseDataResult -> StoredResponseResult.of( + Collections.emptyList(), + convertToSeatBid(storedResponseDataResult), + Collections.emptyMap())); } - public List updateStoredBidResponse(List auctionParticipations) { - return auctionParticipations.stream() - .map(StoredResponseProcessor::updateStoredBidResponse) - .collect(Collectors.toList()); + private Map getImpsExtPrebid(List imps) { + return imps.stream() + .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); } - private static AuctionParticipation updateStoredBidResponse(AuctionParticipation auctionParticipation) { - final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); - final BidRequest bidRequest = bidderRequest.getBidRequest(); - - final List imps = bidRequest.getImp(); - // Аor now, Stored Bid Response works only for bid requests with single imp - if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) { - return auctionParticipation; + private ExtImp getExtImp(ObjectNode extImpNode, String impId) { + try { + return mapper.mapper().treeToValue(extImpNode, ExtImp.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException( + "Error decoding bidRequest.imp.ext for impId = %s : %s".formatted(impId, e.getMessage())); } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid initialSeatBid = bidderResponse.getSeatBid(); - final BidderSeatBid adjustedSeatBid = updateSeatBid(initialSeatBid, imps.get(0).getId()); - - return auctionParticipation.with(bidderResponse.with(adjustedSeatBid)); } - private static BidderSeatBid updateSeatBid(BidderSeatBid bidderSeatBid, String impId) { - final List bids = bidderSeatBid.getBids().stream() - .map(bidderBid -> resolveBidImpId(bidderBid, impId)) - .collect(Collectors.toList()); - - return bidderSeatBid.with(bids); + private Map getAuctionStoredResponses(Map extImpPrebids) { + return extImpPrebids.entrySet().stream() + .map(impIdToExtPrebid -> Tuple2.of( + impIdToExtPrebid.getKey(), + extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) + .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) + .collect(Collectors.toMap(Tuple2::getLeft, Tuple2::getRight)); } - private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { - final Bid bid = bidderBid.getBid(); - final String bidImpId = bid.getImpid(); - if (!StringUtils.contains(bidImpId, PBS_IMPID_MACRO)) { - return bidderBid; - } - - return bidderBid.toBuilder() - .bid(bid.toBuilder().impid(bidImpId.replace(PBS_IMPID_MACRO, impId)).build()) - .build(); + private StoredResponse extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { + final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); + return Optional.ofNullable(storedAuctionResponse) + .map(ExtStoredAuctionResponse::getSeatBid) + .map(StoredResponse.StoredResponseObject::new) + .or(() -> Optional.ofNullable(storedAuctionResponse) + .map(ExtStoredAuctionResponse::getId) + .map(StoredResponse.StoredResponseId::new)) + .orElse(null); } - List mergeWithBidderResponses(List auctionParticipations, - List storedAuctionResponses, - List imps) { - if (CollectionUtils.isEmpty(storedAuctionResponses)) { - return auctionParticipations; - } - - final Map bidderToAuctionParticipation = auctionParticipations.stream() - .collect(Collectors.toMap(AuctionParticipation::getBidder, Function.identity())); - final Map bidderToSeatBid = storedAuctionResponses.stream() - .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); - final Map impIdToBidType = imps.stream() - .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); - final Set responseBidders = new HashSet<>(bidderToAuctionParticipation.keySet()); - responseBidders.addAll(bidderToSeatBid.keySet()); - - return responseBidders.stream() - .map(bidder -> updateBidderResponse(bidderToAuctionParticipation.get(bidder), - bidderToSeatBid.get(bidder), impIdToBidType)) - .toList(); - } + private List excludeStoredAuctionResponseImps(List imps, + Map impIdToStoredResponse) { - private Map getImpsExtPrebid(List imps) { return imps.stream() - .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); - } - - private Map getAuctionStoredResponses(Map extImpPrebids) { - return extImpPrebids.entrySet().stream() - .map(impIdToExtPrebid -> Tuple2.of(impIdToExtPrebid.getKey(), - extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) - .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) - .collect(Collectors.toMap(Tuple2::getRight, Tuple2::getLeft)); + .filter(imp -> !impIdToStoredResponse.containsKey(imp.getId())) + .toList(); } - private String extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { - final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); - return storedAuctionResponse != null ? storedAuctionResponse.getId() : null; - } + private Map> getStoredBidResponses( + Map extImpPrebids, + List imps) { - private Map> getStoredBidResponses(Map extImpPrebids, - List imps) { // PBS supports stored bid response only for requests with single impression, but it can be changed in future if (imps.size() != 1) { return Collections.emptyMap(); } - final Set impsIds = imps.stream().map(Imp::getId).collect(Collectors.toSet()); - return extImpPrebids.entrySet().stream() - .filter(impIdToExtPrebid -> impsIds.contains(impIdToExtPrebid.getKey())) - .filter(impIdToExtPrebid -> CollectionUtils - .isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) - .collect(Collectors.toMap(Map.Entry::getKey, + .filter(impIdToExtPrebid -> + CollectionUtils.isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) + .collect(Collectors.toMap( + Map.Entry::getKey, impIdToStoredResponses -> resolveStoredBidResponse(impIdToStoredResponses.getValue().getStoredBidResponse()))); } - private ExtImp getExtImp(ObjectNode extImpNode, String impId) { - try { - return mapper.mapper().treeToValue(extImpNode, ExtImp.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException( - "Error decoding bidRequest.imp.ext for impId = %s : %s".formatted(impId, e.getMessage())); - } - } + private Map resolveStoredBidResponse( + List storedBidResponse) { - private Map resolveStoredBidResponse(List storedBidResponse) { return storedBidResponse.stream() - .collect(Collectors.toMap(ExtStoredBidResponse::getBidder, ExtStoredBidResponse::getId)); + .collect(Collectors.toMap( + ExtStoredBidResponse::getBidder, + extStoredBidResponse -> new StoredResponse.StoredResponseId(extStoredBidResponse.getId()))); + } + + private Future getStoredResponses(Set storedResponses, Timeout timeout) { + return applicationSettings.getStoredResponses( + storedResponses.stream() + .filter(StoredResponse.StoredResponseId.class::isInstance) + .map(StoredResponse.StoredResponseId.class::cast) + .map(StoredResponse.StoredResponseId::id) + .collect(Collectors.toSet()), + timeout); } private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult, - Map auctionStoredResponses) { + Map impIdsToStoredResponses) { + + final List resolvedSeatBids = new ArrayList<>(); + final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); + for (Map.Entry impIdToStoredResponse : impIdsToStoredResponses.entrySet()) { + final String impId = impIdToStoredResponse.getKey(); + final StoredResponse storedResponse = impIdToStoredResponse.getValue(); + final List seatBids = resolveSeatBids(storedResponse, idToStoredResponses, impId); + + validateStoredSeatBid(seatBids); + resolvedSeatBids.addAll(seatBids.stream() + .map(seatBid -> updateSeatBidBids(seatBid, impId)) + .toList()); + } + return mergeSameBidderSeatBid(resolvedSeatBids); + } + + private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult) { final List resolvedSeatBids = new ArrayList<>(); final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); - for (final Map.Entry storedIdToImpId : auctionStoredResponses.entrySet()) { + for (Map.Entry storedIdToImpId : idToStoredResponses.entrySet()) { final String id = storedIdToImpId.getKey(); - final String impId = storedIdToImpId.getValue(); - final String rowSeatBid = idToStoredResponses.get(id); + final String rowSeatBid = storedIdToImpId.getValue(); if (rowSeatBid == null) { throw new InvalidRequestException( - "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." - .formatted(impId, id)); + "Failed to fetch stored auction response for storedAuctionResponse id = %s.".formatted(id)); } final List seatBids = parseSeatBid(id, rowSeatBid); validateStoredSeatBid(seatBids); - resolvedSeatBids.addAll(seatBids.stream() - .map(seatBid -> updateSeatBidBids(seatBid, impId)) - .toList()); + resolvedSeatBids.addAll(seatBids); } return mergeSameBidderSeatBid(resolvedSeatBids); } + private List resolveSeatBids(StoredResponse storedResponse, + Map idToStoredResponses, + String impId) { + + if (storedResponse instanceof StoredResponse.StoredResponseObject storedResponseObject) { + return Collections.singletonList(storedResponseObject.seatBid()); + } + + final String storedResponseId = ((StoredResponse.StoredResponseId) storedResponse).id(); + final String rowSeatBid = idToStoredResponses.get(storedResponseId); + if (rowSeatBid == null) { + throw new InvalidRequestException( + "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." + .formatted(impId, storedResponseId)); + } + + return parseSeatBid(storedResponseId, rowSeatBid); + } + private List parseSeatBid(String id, String rowSeatBid) { try { return mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPE); @@ -248,18 +247,6 @@ private List parseSeatBid(String id, String rowSeatBid) { } } - private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { - return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); - } - - private List updateBidsWithImpId(List bids, String impId) { - return bids.stream().map(bid -> updateBidWithImpId(bid, impId)).toList(); - } - - private static Bid updateBidWithImpId(Bid bid, String impId) { - return bid.toBuilder().impid(impId).build(); - } - private void validateStoredSeatBid(List seatBids) { for (final SeatBid seatBid : seatBids) { if (StringUtils.isEmpty(seatBid.getSeat())) { @@ -272,6 +259,18 @@ private void validateStoredSeatBid(List seatBids) { } } + private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { + return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); + } + + private List updateBidsWithImpId(List bids, String impId) { + return bids.stream().map(bid -> updateBidWithImpId(bid, impId)).toList(); + } + + private static Bid updateBidWithImpId(Bid bid, String impId) { + return bid.toBuilder().impid(impId).build(); + } + private List mergeSameBidderSeatBid(List seatBids) { return seatBids.stream().collect(Collectors.groupingBy(SeatBid::getSeat, Collectors.toList())) .entrySet().stream() @@ -289,23 +288,108 @@ private SeatBid makeMergedSeatBid(String seat, List storedSeatBids) { private Map> mapStoredBidResponseIdsToValues( Map idToStoredResponses, - Map> impToBidderToStoredBidResponseId) { + Map> impToBidderToStoredBidResponseId) { return impToBidderToStoredBidResponseId.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().entrySet().stream() - .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue())) + .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue().id())) .collect(Collectors.toMap( Map.Entry::getKey, - bidderToId -> idToStoredResponses.get(bidderToId.getValue()), + bidderToId -> idToStoredResponses.get(bidderToId.getValue().id()), (first, second) -> second, CaseInsensitiveMap::new)))); } + public List updateStoredBidResponse(List auctionParticipations) { + return auctionParticipations.stream() + .map(StoredResponseProcessor::updateStoredBidResponse) + .collect(Collectors.toList()); + } + + private static AuctionParticipation updateStoredBidResponse(AuctionParticipation auctionParticipation) { + final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + + final List imps = bidRequest.getImp(); + // Аor now, Stored Bid Response works only for bid requests with single imp + if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid initialSeatBid = bidderResponse.getSeatBid(); + final BidderSeatBid adjustedSeatBid = updateSeatBid(initialSeatBid, imps.getFirst().getId()); + + return auctionParticipation.with(bidderResponse.with(adjustedSeatBid)); + } + + private static BidderSeatBid updateSeatBid(BidderSeatBid bidderSeatBid, String impId) { + final List bids = bidderSeatBid.getBids().stream() + .map(bidderBid -> resolveBidImpId(bidderBid, impId)) + .collect(Collectors.toList()); + + return bidderSeatBid.with(bids); + } + + private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { + final Bid bid = bidderBid.getBid(); + final String bidImpId = bid.getImpid(); + if (!StringUtils.contains(bidImpId, PBS_IMPID_MACRO)) { + return bidderBid; + } + + return bidderBid.toBuilder() + .bid(bid.toBuilder().impid(bidImpId.replace(PBS_IMPID_MACRO, impId)).build()) + .build(); + } + + List mergeWithBidderResponses(List auctionParticipations, + List storedAuctionResponses, + List imps, + Map bidRejectionTrackers) { + + if (CollectionUtils.isEmpty(storedAuctionResponses)) { + return auctionParticipations; + } + + final Map bidderToAuctionParticipation = auctionParticipations.stream() + .collect(Collectors.toMap(AuctionParticipation::getBidder, Function.identity())); + final Map bidderToSeatBid = storedAuctionResponses.stream() + .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); + final Map impIdToBidType = imps.stream() + .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); + final Set responseBidders = new HashSet<>(bidderToAuctionParticipation.keySet()); + responseBidders.addAll(bidderToSeatBid.keySet()); + + return responseBidders.stream() + .map(bidder -> updateBidderResponse( + bidderToAuctionParticipation.get(bidder), + bidderToSeatBid.get(bidder), + impIdToBidType)) + .map(auctionParticipation -> restoreStoredBidsFromRejection(bidRejectionTrackers, auctionParticipation)) + .toList(); + } + + private BidType resolveBidType(Imp imp) { + BidType bidType = BidType.banner; + if (imp.getBanner() != null) { + return bidType; + } else if (imp.getVideo() != null) { + bidType = BidType.video; + } else if (imp.getXNative() != null) { + bidType = BidType.xNative; + } else if (imp.getAudio() != null) { + bidType = BidType.audio; + } + return bidType; + } + private AuctionParticipation updateBidderResponse(AuctionParticipation auctionParticipation, SeatBid storedSeatBid, Map impIdToBidType) { + if (auctionParticipation != null) { if (auctionParticipation.isRequestBlocked()) { return auctionParticipation; @@ -330,13 +414,17 @@ private AuctionParticipation updateBidderResponse(AuctionParticipation auctionPa } } - private BidderSeatBid makeBidderSeatBid(BidderSeatBid bidderSeatBid, SeatBid seatBid, + private BidderSeatBid makeBidderSeatBid(BidderSeatBid bidderSeatBid, + SeatBid seatBid, Map impIdToBidType) { + final boolean nonNullBidderSeatBid = bidderSeatBid != null; final String bidCurrency = nonNullBidderSeatBid ? bidderSeatBid.getBids().stream() - .map(BidderBid::getBidCurrency).filter(Objects::nonNull) - .findAny().orElse(DEFAULT_BID_CURRENCY) + .map(BidderBid::getBidCurrency) + .filter(Objects::nonNull) + .findAny() + .orElse(DEFAULT_BID_CURRENCY) : DEFAULT_BID_CURRENCY; final List bidderBids = seatBid != null ? seatBid.getBid().stream() @@ -369,17 +457,28 @@ private ExtBidPrebid parseExtBidPrebid(ObjectNode bidExtPrebid) { } } - private BidType resolveBidType(Imp imp) { - BidType bidType = BidType.banner; - if (imp.getBanner() != null) { - return bidType; - } else if (imp.getVideo() != null) { - bidType = BidType.video; - } else if (imp.getXNative() != null) { - bidType = BidType.xNative; - } else if (imp.getAudio() != null) { - bidType = BidType.audio; + private static AuctionParticipation restoreStoredBidsFromRejection( + Map bidRejectionTrackers, + AuctionParticipation auctionParticipation) { + + final BidRejectionTracker bidRejectionTracker = bidRejectionTrackers.get(auctionParticipation.getBidder()); + + if (bidRejectionTracker != null) { + Optional.ofNullable(auctionParticipation.getBidderResponse()) + .map(BidderResponse::getSeatBid) + .map(BidderSeatBid::getBids) + .ifPresent(bidRejectionTracker::restoreFromRejection); + } + + return auctionParticipation; + } + + private sealed interface StoredResponse { + + record StoredResponseId(String id) implements StoredResponse { + } + + record StoredResponseObject(SeatBid seatBid) implements StoredResponse { } - return bidType; } } diff --git a/src/main/java/org/prebid/server/auction/SupplyChainResolver.java b/src/main/java/org/prebid/server/auction/SupplyChainResolver.java index 2faa20174d4..6c7246ced6f 100644 --- a/src/main/java/org/prebid/server/auction/SupplyChainResolver.java +++ b/src/main/java/org/prebid/server/auction/SupplyChainResolver.java @@ -4,13 +4,13 @@ import com.iab.openrtb.request.Source; import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.SupplyChainNode; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; @@ -72,7 +72,7 @@ private SupplyChain existingSchainOrNull(String bidder, } if (existingSchain != null) { - logger.debug("Schain bidder {0} is rejected since it was defined more than once", bidder); + logger.debug("Schain bidder {} is rejected since it was defined more than once", bidder); return null; } diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 9472f734336..2896e153adf 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -32,10 +32,6 @@ public class TargetingKeywordsCreator { * It will exist only if the incoming bidRequest defiend request.app instead of request.site. */ private static final String ENV_KEY = "_env"; - /** - * Used as a value for ENV_KEY. - */ - private static final String ENV_APP_VALUE = "mobile-app"; /** * Name of the Bidder. For example, "appnexus" or "rubicon". */ @@ -87,7 +83,7 @@ public class TargetingKeywordsCreator { private final boolean includeBidderKeys; private final boolean alwaysIncludeDeals; private final boolean includeFormat; - private final boolean isApp; + private final String env; private final int truncateAttrChars; private final String cacheHost; private final String cachePath; @@ -99,7 +95,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -111,7 +107,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, this.includeBidderKeys = includeBidderKeys; this.alwaysIncludeDeals = alwaysIncludeDeals; this.includeFormat = includeFormat; - this.isApp = isApp; + this.env = env; this.truncateAttrChars = truncateAttrChars; this.cacheHost = cacheHost; this.cachePath = cachePath; @@ -127,7 +123,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -139,7 +135,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul includeBidderKeys, alwaysIncludeDeals, includeFormat, - isApp, + env, truncateAttrChars, cacheHost, cachePath, @@ -230,8 +226,8 @@ private Map makeFor(String bidder, if (StringUtils.isNotBlank(dealId)) { keywordMap.put(this.keyPrefix + DEAL_KEY, dealId); } - if (isApp) { - keywordMap.put(this.keyPrefix + ENV_KEY, ENV_APP_VALUE); + if (env != null) { + keywordMap.put(this.keyPrefix + ENV_KEY, env); } if (StringUtils.isNotBlank(categoryDuration)) { keywordMap.put(this.keyPrefix + CATEGORY_DURATION_KEY, categoryDuration); diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index 91ded451731..84ebacc7416 100644 --- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java @@ -17,8 +17,6 @@ import com.iab.openrtb.request.video.Podconfig; import io.vertx.core.Future; import io.vertx.core.file.FileSystem; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; @@ -29,6 +27,8 @@ import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -59,7 +59,7 @@ public class VideoStoredRequestProcessor { private static final String DEFAULT_CURRENCY = "USD"; private final boolean enforceStoredRequest; - private final List blacklistedAccounts; + private final List blocklistedAccounts; private final long defaultTimeout; private final String currency; private final BidRequest defaultBidRequest; @@ -71,7 +71,7 @@ public class VideoStoredRequestProcessor { private final JsonMerger jsonMerger; public VideoStoredRequestProcessor(boolean enforceStoredRequest, - List blacklistedAccounts, + List blocklistedAccounts, long defaultTimeout, String adServerCurrency, String defaultBidRequestPath, @@ -84,7 +84,7 @@ public VideoStoredRequestProcessor(boolean enforceStoredRequest, JsonMerger jsonMerger) { this.enforceStoredRequest = enforceStoredRequest; - this.blacklistedAccounts = Objects.requireNonNull(blacklistedAccounts); + this.blocklistedAccounts = Objects.requireNonNull(blocklistedAccounts); this.defaultTimeout = defaultTimeout; this.currency = StringUtils.isBlank(adServerCurrency) ? DEFAULT_CURRENCY : adServerCurrency; this.defaultBidRequest = readBidRequest( @@ -147,7 +147,7 @@ private WithPodErrors toBidRequestWithPodErrors(StoredDataResult sto String storedBidRequestId) { final BidRequestVideo mergedStoredRequest = mergeBidRequest(videoRequest, storedBidRequestId, storedResult); - validator.validateStoredBidRequest(mergedStoredRequest, enforceStoredRequest, blacklistedAccounts); + validator.validateStoredBidRequest(mergedStoredRequest, enforceStoredRequest, blocklistedAccounts); final Podconfig podconfig = mergedStoredRequest.getPodconfig(); final Video video = mergedStoredRequest.getVideo(); diff --git a/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java b/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java index 07c3b83cc98..6e6c6a2e13f 100644 --- a/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java +++ b/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java @@ -1,14 +1,9 @@ package org.prebid.server.auction; -import com.iab.openrtb.request.Deal; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Pmp; -import com.iab.openrtb.response.Bid; -import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.auction.model.BidInfo; import java.util.Comparator; -import java.util.List; import java.util.Objects; /** @@ -18,14 +13,11 @@ public class WinningBidComparatorFactory { private static final Comparator WINNING_BID_PRICE_COMPARATOR = new WinningBidPriceComparator(); private static final Comparator WINNING_BID_DEAL_COMPARATOR = new WinningBidDealComparator(); - private static final Comparator WINNING_BID_PG_COMPARATOR = new WinningBidPgComparator(); - private static final Comparator BID_INFO_COMPARATOR = WINNING_BID_PG_COMPARATOR - .thenComparing(WINNING_BID_DEAL_COMPARATOR) + private static final Comparator BID_INFO_COMPARATOR = WINNING_BID_DEAL_COMPARATOR .thenComparing(WINNING_BID_PRICE_COMPARATOR); - private static final Comparator PREFER_PRICE_COMPARATOR = WINNING_BID_PG_COMPARATOR - .thenComparing(WINNING_BID_PRICE_COMPARATOR); + private static final Comparator PREFER_PRICE_COMPARATOR = WINNING_BID_PRICE_COMPARATOR; public Comparator create(boolean preferDeals) { return preferDeals @@ -43,63 +35,9 @@ private static class WinningBidDealComparator implements Comparator { @Override public int compare(BidInfo bidInfo1, BidInfo bidInfo2) { - final boolean isPresentBidDealId1 = bidInfo1.getBid().getDealid() != null; - final boolean isPresentBidDealId2 = bidInfo2.getBid().getDealid() != null; - - if (!Boolean.logicalXor(isPresentBidDealId1, isPresentBidDealId2)) { - return 0; - } - - return isPresentBidDealId1 ? 1 : -1; - } - } - - /** - * Compares two {@link BidInfo} arguments for order based on PG deal priority. - * Returns negative integer when first does not have a pg deal and second has, or when both have a pg deal, - * but first has higher index in deals array that means lower priority. - * Returns positive integer when first has a pg deal and second does not, or when both have a pg deal, - * but first has lower index in deals array that means higher priority. - * Returns zero when both dont have pg deals. - */ - private static class WinningBidPgComparator implements Comparator { - - private final Comparator dealIndexComparator = Comparator.comparingInt(Integer::intValue).reversed(); - - @Override - public int compare(BidInfo bidInfo1, BidInfo bidInfo2) { - final Imp imp = bidInfo1.getCorrespondingImp(); - final Pmp pmp = imp.getPmp(); - final List impDeals = pmp != null ? pmp.getDeals() : null; - - if (CollectionUtils.isEmpty(impDeals)) { - return 0; - } - - final Bid bid1 = bidInfo1.getBid(); - final Bid bid2 = bidInfo2.getBid(); - - int indexOfBidDealId1 = -1; - int indexOfBidDealId2 = -1; - - // search for indexes of deals - for (int i = 0; i < impDeals.size(); i++) { - final String dealId = impDeals.get(i).getId(); - if (Objects.equals(dealId, bid1.getDealid())) { - indexOfBidDealId1 = i; - } - if (Objects.equals(dealId, bid2.getDealid())) { - indexOfBidDealId2 = i; - } - } - - final boolean isPresentImpDealId1 = indexOfBidDealId1 != -1; - final boolean isPresentImpDealId2 = indexOfBidDealId2 != -1; - - final boolean isOneOrBothDealIdNotPresent = !isPresentImpDealId1 || !isPresentImpDealId2; - return isOneOrBothDealIdNotPresent - ? isPresentImpDealId1 ? 1 : -1 // case when no deal IDs found is covered by response validator - : dealIndexComparator.compare(indexOfBidDealId1, indexOfBidDealId2); + final int bidDeal1Weight = bidInfo1.getBid().getDealid() != null ? 1 : 0; + final int bidDeal2Weight = bidInfo2.getBid().getDealid() != null ? 1 : 0; + return bidDeal1Weight - bidDeal2Weight; } } diff --git a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java index 7a42ef8e464..77bbb7372ce 100644 --- a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java +++ b/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java @@ -6,10 +6,8 @@ import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.math.BigDecimal; -import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; -import java.util.EnumSet; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -17,17 +15,6 @@ public class FloorAdjustmentFactorResolver { - public BigDecimal resolve(ImpMediaType impMediaType, - ExtRequestBidAdjustmentFactors adjustmentFactors, - String bidder) { - - final Set impMediaTypes = impMediaType != null - ? EnumSet.of(impMediaType) - : Collections.emptySet(); - - return resolve(impMediaTypes, adjustmentFactors, bidder); - } - public BigDecimal resolve(Set impMediaTypes, ExtRequestBidAdjustmentFactors adjustmentFactors, String bidder) { diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java index 41e8bd02a91..b13cf522b49 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -205,7 +205,7 @@ private Future resolveCategory(String primaryAdServer, return Future.failedFuture( new RejectedBidException(bid.getId(), bidder, "Bid has more than one category")); } - final String category = CollectionUtils.isNotEmpty(iabCategories) ? iabCategories.get(0) : null; + final String category = CollectionUtils.isNotEmpty(iabCategories) ? iabCategories.getFirst() : null; if (StringUtils.isBlank(category)) { return Future.failedFuture( new RejectedBidException(bid.getId(), bidder, "Bid did not contain a category")); @@ -556,7 +556,7 @@ private Integer resolveDuration(List durations, BidderBid bidderBid, St final String bidId = bidderBid.getBid().getId(); - final int maxDuration = durations.get(durations.size() - 1); + final int maxDuration = durations.getLast(); if (duration > maxDuration) { throw new RejectedBidException( bidId, bidder, "Bid duration '%s' exceeds maximum '%s'".formatted(duration, maxDuration)); diff --git a/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java b/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java index fb1cc09c74b..f923711447b 100644 --- a/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java +++ b/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java @@ -1,6 +1,7 @@ package org.prebid.server.auction.gpp; import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.gpp.model.GppContextCreator; import org.prebid.server.auction.gpp.model.GppContextWrapper; @@ -44,7 +45,7 @@ private static GppContextWrapper contextFrom(PrivacyContext privacyContext) { private static Integer toInt(String string) { try { - return string != null ? Integer.parseInt(string) : null; + return StringUtils.isNotBlank(string) ? Integer.parseInt(string) : null; } catch (NumberFormatException e) { return null; } diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java index f4e89ea2f76..af0a9e2428c 100644 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java +++ b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.model.BidderError; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -78,10 +80,11 @@ private MediaType preferredMediaType(BidRequest bidRequest, Account account, String originalBidderName, String resolvedBidderName) { + return Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) .map(ExtRequestPrebid::getBiddercontrols) - .map(bidders -> bidders.get(originalBidderName)) + .map(bidders -> getBidder(originalBidderName, bidders)) .map(bidder -> bidder.get(PREF_MTYPE_FIELD)) .filter(JsonNode::isTextual) .map(JsonNode::textValue) @@ -92,6 +95,17 @@ private MediaType preferredMediaType(BidRequest bidRequest, .orElse(null); } + private static JsonNode getBidder(String bidderName, JsonNode biddersNode) { + final Iterator fieldNames = biddersNode.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (StringUtils.equalsIgnoreCase(bidderName, fieldName)) { + return biddersNode.get(fieldName); + } + } + return null; + } + private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { if (!isMultiFormat(imp)) { return imp; diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 39a2b09b81c..3ee60aab4fa 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -10,8 +10,6 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.deals.model.DeepDebugLog; -import org.prebid.server.deals.model.TxnLog; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.metric.MetricName; @@ -60,6 +58,7 @@ public class AuctionContext { ActivityInfrastructure activityInfrastructure; + @JsonIgnore GeoInfo geoInfo; HookExecutionContext hookExecutionContext; @@ -68,11 +67,7 @@ public class AuctionContext { boolean requestRejected; - @JsonIgnore - TxnLog txnLog; - - @JsonIgnore - DeepDebugLog deepDebugLog; + boolean auctionSkipped; CachedDebugLog cachedDebugLog; @@ -123,9 +118,21 @@ public AuctionContext with(DebugContext debugContext) { .build(); } + public AuctionContext with(GeoInfo geoInfo) { + return this.toBuilder() + .geoInfo(geoInfo) + .build(); + } + public AuctionContext withRequestRejected() { return this.toBuilder() .requestRejected(true) .build(); } + + public AuctionContext skipAuction() { + return this.toBuilder() + .auctionSkipped(true) + .build(); + } } diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java index 025de675be2..1cb95bcf681 100644 --- a/src/main/java/org/prebid/server/auction/model/BidInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -25,16 +25,16 @@ public class BidInfo { CacheInfo cacheInfo; - String lineItemId; - - String lineItemSource; - TargetingInfo targetingInfo; String category; Boolean satisfiedPriority; + Integer ttl; + + Integer videoTtl; + public String getBidId() { final ObjectNode extNode = bid != null ? bid.getExt() : null; final JsonNode bidIdNode = extNode != null ? extNode.path("prebid").path("bidid") : null; diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index 4858c80b0c4..e916ee0b3a5 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -1,19 +1,105 @@ package org.prebid.server.auction.model; import com.fasterxml.jackson.annotation.JsonValue; -import org.prebid.server.bidder.model.BidderError; +/** + * The list of the Seat Non Bid codes: + * 0 - the bidder is called but declines to bid and doesn't provide a code (for the impression) + * 100-199 - the bidder is called but returned with an unspecified error (for the impression) + * 200-299 - the bidder is not called at all + * 300-399 - the bidder is called, but its response is rejected + */ public enum BidRejectionReason { + /** + * If the bidder returns in time but declines to bid and doesn’t provide an “NBR” code. + */ NO_BID(0), - TIMED_OUT(101), - REJECTED_BY_HOOK(200), - REJECTED_BY_PRIVACY(202), - REJECTED_BY_MEDIA_TYPE(204), - GENERAL(300), - REJECTED_DUE_TO_PRICE_FLOOR(301), - FAILED_TO_REQUEST_BIDS(100), - OTHER_ERROR(100); + + /** + * The bidder returned with an unspecified error for this impression. + * Applied if any other ERROR is not recognized. + */ + ERROR_GENERAL(100), + + /** + * The bidder failed because of timeout + */ + ERROR_TIMED_OUT(101), + + /** + * The bidder returned status code less than 200 OR greater than or equal to 400 + */ + ERROR_INVALID_BID_RESPONSE(102), + + /** + * The bidder returned HTTP 503 + */ + ERROR_BIDDER_UNREACHABLE(103), + + /** + * The bidder is not called at all. + * Applied if any other REQUEST_BLOCKED reason is not recognized. + */ + REQUEST_BLOCKED_GENERAL(200), + + /** + * If the request was not sent to the bidder because they don’t support dooh or app + */ + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + + /** + * This impression not sent to the bid adapter because it doesn’t support the requested mediatype. + */ + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + + /** + * If the bidder was not called due to GDPR purpose 2 + */ + REQUEST_BLOCKED_PRIVACY(204), + + /** + * If the bidder was not called due to a mismatch between the bidder’s currency and the request’s currency. + */ + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), + + /** + * The bidder is called, but its response is rejected. + * Applied if any other RESPONSE_REJECTED reason is not recognized. + */ + RESPONSE_REJECTED_GENERAL(300), + + /** + * The bidder returns a bid that doesn't meet the price floor. + */ + RESPONSE_REJECTED_BELOW_FLOOR(301), + + /** + * Rejected by the DSA validations + */ + RESPONSE_REJECTED_DSA_PRIVACY(305), + + /** + * If the ortbblocking module enforced a bid response for battr, bcat, bapp, btype. + * If the richmedia module filtered out a bid response. + */ + RESPONSE_REJECTED_INVALID_CREATIVE(350), + + /** + * If a bid response was rejected due to size. + * When the auction.bid-validations.banner-creative-max-size is in enforce mode and rejects a bid. + */ + RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED(351), + + /** + * If a bid response was rejected due to auction.validations.secure-markup + */ + RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE(352), + + /** + * If the ortbblocking module enforced a bid response due to badv + */ + RESPONSE_REJECTED_ADVERTISER_BLOCKED(356); public final int code; @@ -26,11 +112,4 @@ private int getValue() { return code; } - public static BidRejectionReason fromBidderError(BidderError error) { - return switch (error.getType()) { - case timeout -> BidRejectionReason.TIMED_OUT; - case rejected_ipf -> BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR; - default -> BidRejectionReason.OTHER_ERROR; - }; - } } diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java index 62cd645bcf3..b0504ec13a3 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java @@ -1,10 +1,10 @@ package org.prebid.server.auction.model; import com.iab.openrtb.response.Bid; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.MapUtil; import java.util.Collection; diff --git a/src/main/java/org/prebid/server/auction/model/BidderRequest.java b/src/main/java/org/prebid/server/auction/model/BidderRequest.java index 1d18519a0f8..ad60230e54b 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderRequest.java +++ b/src/main/java/org/prebid/server/auction/model/BidderRequest.java @@ -1,14 +1,10 @@ package org.prebid.server.auction.model; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Deal; import lombok.Builder; import lombok.Value; import org.prebid.server.auction.versionconverter.OrtbVersion; -import java.util.List; -import java.util.Map; - @Builder(toBuilder = true) @Value public class BidderRequest { @@ -19,8 +15,6 @@ public class BidderRequest { String storedResponse; - Map> impIdToDeals; - BidRequest bidRequest; public BidderRequest with(BidRequest bidRequest) { diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java index 1f1db91f7aa..84f9695c3ea 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java @@ -61,7 +61,8 @@ public Future contextFrom(AuctionContext auctionContext) { accountGdprConfig(account), requestType, requestLogInfo(requestType, bidRequest, account.getId()), - auctionContext.getTimeoutContext().getTimeout()) + auctionContext.getTimeoutContext().getTimeout(), + auctionContext.getGeoInfo()) .map(tcfContext -> logWarnings(auctionContext.getDebugWarnings(), tcfContext)) .map(tcfContext -> PrivacyContext.of(strippedPrivacy, tcfContext, tcfContext.getIpAddress())); } diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java index debcefe86e4..087b72a67c6 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java @@ -57,7 +57,8 @@ public Future contextFrom(AuctionContext auctionContext) { accountGdprConfig(account), requestType, requestLogInfo(requestType, bidRequest, account.getId()), - auctionContext.getTimeoutContext().getTimeout()) + auctionContext.getTimeoutContext().getTimeout(), + auctionContext.getGeoInfo()) .map(tcfContext -> logWarnings(auctionContext.getDebugWarnings(), tcfContext)) .map(tcfContext -> PrivacyContext.of(privacy, tcfContext, tcfContext.getIpAddress())); } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java index 29e07658eb6..df7603048ee 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java @@ -57,15 +57,8 @@ private BidderPrivacyResult applyActivityRestrictions(BidderPrivacyResult bidder final boolean disallowTransmitEids = !infrastructure.isAllowed(Activity.TRANSMIT_EIDS, payload); final boolean disallowTransmitGeo = !infrastructure.isAllowed(Activity.TRANSMIT_GEO, payload); - final User resolvedUser = userFpdActivityMask.maskUser( - user, - disallowTransmitUfpd, - disallowTransmitEids, - disallowTransmitGeo); - final Device resolvedDevice = userFpdActivityMask.maskDevice( - device, - disallowTransmitUfpd, - disallowTransmitGeo); + final User resolvedUser = userFpdActivityMask.maskUser(user, disallowTransmitUfpd, disallowTransmitEids); + final Device resolvedDevice = userFpdActivityMask.maskDevice(device, disallowTransmitUfpd, disallowTransmitGeo); return bidderPrivacyResult.toBuilder() .user(resolvedUser) diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java index 0267fbd8d3d..10fe183801d 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java @@ -52,30 +52,27 @@ public Future> enforce(AuctionContext auctionContext, BidderAliases aliases) { final Ccpa ccpa = auctionContext.getPrivacyContext().getPrivacy().getCcpa(); - metrics.updatePrivacyCcpaMetrics(ccpa.isNotEmpty(), ccpa.isEnforced()); + final BidRequest bidRequest = auctionContext.getBidRequest(); - return Future.succeededFuture(enforce(bidderToUser, ccpa, auctionContext, aliases)); - } + final boolean isCcpaEnforced = ccpa.isEnforced(); + final boolean isCcpaEnabled = isCcpaEnabled(auctionContext.getAccount(), auctionContext.getRequestTypeMetric()); - private List enforce(Map bidderToUser, - Ccpa ccpa, - AuctionContext auctionContext, - BidderAliases aliases) { + final Set enforcedBidders = isCcpaEnabled && isCcpaEnforced + ? extractCcpaEnforcedBidders(bidderToUser.keySet(), bidRequest, aliases) + : Collections.emptySet(); - final BidRequest bidRequest = auctionContext.getBidRequest(); - final Device device = bidRequest.getDevice(); + metrics.updatePrivacyCcpaMetrics( + auctionContext.getActivityInfrastructure(), + ccpa.isNotEmpty(), + isCcpaEnforced, + isCcpaEnabled, + enforcedBidders); - return isCcpaEnforced(ccpa, auctionContext.getAccount(), auctionContext.getRequestTypeMetric()) - ? maskCcpa(bidderToUser, extractCcpaEnforcedBidders(bidderToUser.keySet(), bidRequest, aliases), device) - : Collections.emptyList(); + return Future.succeededFuture(maskCcpa(bidderToUser, enforcedBidders, bidRequest.getDevice())); } public boolean isCcpaEnforced(Ccpa ccpa, Account account) { - return isCcpaEnforced(ccpa, account, null); - } - - private boolean isCcpaEnforced(Ccpa ccpa, Account account, MetricName requestType) { - return ccpa.isEnforced() && isCcpaEnabled(account, requestType); + return ccpa.isEnforced() && isCcpaEnabled(account, null); } private boolean isCcpaEnabled(Account account, MetricName requestType) { diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java index 46b578a9ccc..92471e85ec2 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java @@ -27,7 +27,7 @@ public boolean isApplicable(AuctionContext auctionContext) { } public Future> enforce(AuctionContext auctionContext, Map bidderToUser) { - metrics.updatePrivacyCoppaMetric(); + metrics.updatePrivacyCoppaMetric(auctionContext.getActivityInfrastructure(), bidderToUser.keySet()); return Future.succeededFuture(results(bidderToUser, auctionContext.getBidRequest().getDevice())); } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java index afa0c83e8b1..48e098f63a6 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java @@ -3,15 +3,16 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdTcfMask; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.TcfDefinerService; @@ -66,6 +67,7 @@ public Future> enforce(AuctionContext auctionContext, final Device device = auctionContext.getBidRequest().getDevice(); final AccountGdprConfig accountGdprConfig = accountGdprConfig(auctionContext.getAccount()); final MetricName requestType = auctionContext.getRequestTypeMetric(); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); return tcfDefinerService.resultForBidderNames( bidders, @@ -73,7 +75,13 @@ public Future> enforce(AuctionContext auctionContext, auctionContext.getPrivacyContext().getTcfContext(), accountGdprConfig) .map(TcfResponse::getActions) - .map(enforcements -> updateMetrics(enforcements, aliases, requestType, bidderToUser, device)) + .map(enforcements -> updateMetrics( + activityInfrastructure, + enforcements, + aliases, + requestType, + bidderToUser, + device)) .map(enforcements -> bidderToPrivacyResult(enforcements, bidders, bidderToUser, device)); } @@ -82,12 +90,15 @@ private static AccountGdprConfig accountGdprConfig(Account account) { return privacyConfig != null ? privacyConfig.getGdpr() : null; } - private Map updateMetrics(Map enforcements, + private Map updateMetrics(ActivityInfrastructure activityInfrastructure, + Map enforcements, BidderAliases aliases, MetricName requestType, Map bidderToUser, Device device) { + final boolean isLmtEnforcedAndEnabled = isLmtEnforcedAndEnabled(device); + // Metrics should represent real picture of the bidding process, so if bidder request is blocked // by privacy then no reason to increment another metrics, like geo masked, etc. for (final Map.Entry bidderEnforcement : enforcements.entrySet()) { @@ -103,24 +114,22 @@ private Map updateMetrics(Map eidExceptions = privacyEnforcementAction.getEidExceptions(); - final User maskedUser = userFpdTcfMask.maskUser(user, maskUserFpd, maskUserIds, maskGeo, eidExceptions); + final User maskedUser = userFpdTcfMask.maskUser(user, maskUserFpd, maskUserIds, eidExceptions); final boolean maskIp = privacyEnforcementAction.isMaskDeviceIp() || isLmtEnabled; final boolean maskDeviceInfo = privacyEnforcementAction.isMaskDeviceInfo() || isLmtEnabled; diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdActivityMask.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdActivityMask.java index 559d35b02fc..6e8a5558ba2 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdActivityMask.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdActivityMask.java @@ -14,16 +14,11 @@ public UserFpdActivityMask(UserFpdTcfMask userFpdTcfMask) { this.userFpdTcfMask = Objects.requireNonNull(userFpdTcfMask); } - public User maskUser(User user, - boolean disallowTransmitUfpd, - boolean disallowTransmitEids, - boolean disallowTransmitGeo) { - + public User maskUser(User user, boolean disallowTransmitUfpd, boolean disallowTransmitEids) { return userFpdTcfMask.maskUser( user, disallowTransmitUfpd, disallowTransmitEids, - disallowTransmitGeo, Collections.emptySet()); } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCcpaMask.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCcpaMask.java index 4ed50a5cb58..fbb20d55a9b 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCcpaMask.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCcpaMask.java @@ -1,42 +1,23 @@ package org.prebid.server.auction.privacy.enforcement.mask; import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.User; -import org.prebid.server.auction.IpAddressHelper; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.Collections; +import java.util.Objects; -public class UserFpdCcpaMask extends UserFpdPrivacyMask { +public class UserFpdCcpaMask { - public UserFpdCcpaMask(IpAddressHelper ipAddressHelper) { - super(ipAddressHelper); + private final UserFpdActivityMask userFpdActivityMask; + + public UserFpdCcpaMask(UserFpdActivityMask userFpdActivityMask) { + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } public User maskUser(User user) { - return maskUser(user, true, true, true, Collections.emptySet()); + return userFpdActivityMask.maskUser(user, true, true); } public Device maskDevice(Device device) { - return maskDevice(device, true, true, true); - } - - @Override - protected Geo maskGeo(Geo geo) { - return geo.toBuilder() - .lat(maskGeoCoordinate(geo.getLat())) - .lon(maskGeoCoordinate(geo.getLon())) - .metro(null) - .city(null) - .zip(null) - .build(); - } - - private static Float maskGeoCoordinate(Float coordinate) { - return coordinate != null - ? BigDecimal.valueOf(coordinate).setScale(2, RoundingMode.HALF_UP).floatValue() - : null; + return userFpdActivityMask.maskDevice(device, true, true); } } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCoppaMask.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCoppaMask.java index 0fc8a6ad6fc..ded930bff83 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCoppaMask.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdCoppaMask.java @@ -1,34 +1,23 @@ package org.prebid.server.auction.privacy.enforcement.mask; import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.User; -import org.prebid.server.auction.IpAddressHelper; -import java.util.Collections; +import java.util.Objects; -public class UserFpdCoppaMask extends UserFpdPrivacyMask { +public class UserFpdCoppaMask { - public UserFpdCoppaMask(IpAddressHelper ipAddressHelper) { - super(ipAddressHelper); + private final UserFpdActivityMask userFpdActivityMask; + + public UserFpdCoppaMask(UserFpdActivityMask userFpdActivityMask) { + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } public User maskUser(User user) { - return maskUser(user, true, true, true, Collections.emptySet()); + return userFpdActivityMask.maskUser(user, true, true); } public Device maskDevice(Device device) { - return maskDevice(device, true, true, true); - } - - @Override - protected Geo maskGeo(Geo geo) { - return geo.toBuilder() - .lat(null) - .lon(null) - .metro(null) - .city(null) - .zip(null) - .build(); + return userFpdActivityMask.maskDevice(device, true, true); } } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdPrivacyMask.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdPrivacyMask.java index 4d2a04ee203..6ce4e99c44f 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdPrivacyMask.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdPrivacyMask.java @@ -8,6 +8,8 @@ import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; import java.util.Objects; import java.util.Set; @@ -23,10 +25,9 @@ protected UserFpdPrivacyMask(IpAddressHelper ipAddressHelper) { protected User maskUser(User user, boolean maskUserFpd, boolean maskEids, - boolean maskGeo, Set eidExceptions) { - if (user == null || !(maskUserFpd || maskEids || maskGeo)) { + if (user == null || !(maskUserFpd || maskEids)) { return user; } @@ -40,6 +41,7 @@ protected User maskUser(User user, .keywords(null) .kwarray(null) .data(null) + .geo(null) .ext(maskExtUser(user.getExt())); } @@ -47,10 +49,6 @@ protected User maskUser(User user, userBuilder.eids(removeEids(user.getEids(), eidExceptions)); } - if (maskGeo) { - userBuilder.geo(maskNullableGeo(user.getGeo())); - } - return nullIfEmpty(userBuilder.build()); } @@ -73,12 +71,6 @@ private static List removeEids(List eids, Set exceptions) { return clearedEids.isEmpty() ? null : clearedEids; } - private Geo maskNullableGeo(Geo geo) { - return geo != null ? nullIfEmpty(maskGeo(geo)) : null; - } - - protected abstract Geo maskGeo(Geo geo); - protected Device maskDevice(Device device, boolean maskIp, boolean maskGeo, boolean maskDeviceInfo) { if (device == null || !(maskIp || maskGeo || maskDeviceInfo)) { return device; @@ -105,6 +97,29 @@ protected Device maskDevice(Device device, boolean maskIp, boolean maskGeo, bool return deviceBuilder.build(); } + private static Geo maskNullableGeo(Geo geo) { + return geo != null ? nullIfEmpty(maskGeo(geo)) : null; + } + + private static Geo maskGeo(Geo geo) { + return geo.toBuilder() + .lat(maskGeoCoordinate(geo.getLat())) + .lon(maskGeoCoordinate(geo.getLon())) + .metro(null) + .city(null) + .zip(null) + .accuracy(null) + .ipservice(null) + .ext(null) + .build(); + } + + private static Float maskGeoCoordinate(Float coordinate) { + return coordinate != null + ? BigDecimal.valueOf(coordinate).setScale(2, RoundingMode.HALF_UP).floatValue() + : null; + } + private static User nullIfEmpty(User user) { return user.equals(User.EMPTY) ? null : user; } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMask.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMask.java index 744b492807d..c0dfed0c175 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMask.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMask.java @@ -1,12 +1,9 @@ package org.prebid.server.auction.privacy.enforcement.mask; import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.User; import org.prebid.server.auction.IpAddressHelper; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Set; public class UserFpdTcfMask extends UserFpdPrivacyMask { @@ -15,27 +12,11 @@ public UserFpdTcfMask(IpAddressHelper ipAddressHelper) { super(ipAddressHelper); } - public User maskUser(User user, boolean maskUserFpd, boolean maskEids, boolean maskGeo, Set eidExceptions) { - return super.maskUser(user, maskUserFpd, maskEids, maskGeo, eidExceptions); + public User maskUser(User user, boolean maskUserFpd, boolean maskEids, Set eidExceptions) { + return super.maskUser(user, maskUserFpd, maskEids, eidExceptions); } public Device maskDevice(Device device, boolean maskIp, boolean maskGeo, boolean maskDeviceInfo) { return super.maskDevice(device, maskIp, maskGeo, maskDeviceInfo); } - - @Override - protected Geo maskGeo(Geo geo) { - return geo != null - ? geo.toBuilder() - .lat(maskGeoCoordinate(geo.getLat())) - .lon(maskGeoCoordinate(geo.getLon())) - .build() - : null; - } - - private static Float maskGeoCoordinate(Float coordinate) { - return coordinate != null - ? BigDecimal.valueOf(coordinate).setScale(2, RoundingMode.HALF_UP).floatValue() - : null; - } } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index ec9a3875a07..b014c508678 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.DebugResolver; import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.PriceGranularity; @@ -51,6 +52,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -59,6 +61,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; public class AmpRequestFactory { @@ -96,6 +99,7 @@ public class AmpRequestFactory { private final AmpPrivacyContextFactory ampPrivacyContextFactory; private final DebugResolver debugResolver; private final JacksonMapper mapper; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, @@ -107,7 +111,8 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, FpdResolver fpdResolver, AmpPrivacyContextFactory ampPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); @@ -120,6 +125,7 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, this.debugResolver = Objects.requireNonNull(debugResolver); this.ampPrivacyContextFactory = Objects.requireNonNull(ampPrivacyContextFactory); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); } /** @@ -142,6 +148,12 @@ public Future fromRequest(RoutingContext routingContext, long st .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + .compose(auctionContext -> gppService.contextFrom(auctionContext) .map(auctionContext::with)) @@ -154,17 +166,13 @@ public Future fromRequest(RoutingContext routingContext, long st .compose(auctionContext -> ampPrivacyContextFactory.contextFrom(auctionContext) .map(auctionContext::with)) - .map(auctionContext -> auctionContext.with( - ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext))) + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) + .map(auctionContext::with)) .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) - .compose(ortb2RequestFactory::populateUserAdditionalInfo) - - .map(ortb2RequestFactory::enrichWithPriceFloors) - - .map(auctionContext -> ortb2RequestFactory.updateTimeout(auctionContext, startTime)) + .map(ortb2RequestFactory::updateTimeout) .recover(ortb2RequestFactory::restoreResultFromRejection); } @@ -185,9 +193,12 @@ private Future parseBidRequest(AuctionContext auctionContext, HttpRe final String addtlConsent = addtlConsentFromQueryStringParams(httpRequest); final Integer gdpr = gdprFromQueryStringParams(httpRequest); - final GppSidExtraction gppSidExtraction = gppSidFromQueryStringParams(httpRequest); - final String gpc = implicitParametersExtractor.gpcFrom(httpRequest); final Integer debug = debugFromQueryStringParam(httpRequest); + final GppSidExtraction gppSidExtraction = gppSidFromQueryStringParams( + httpRequest, + debug != null && debug == 1, + auctionContext.getDebugWarnings()); + final String gpc = implicitParametersExtractor.gpcFrom(httpRequest); final Long timeout = timeoutFromQueryString(httpRequest); final BidRequest bidRequest = BidRequest.builder() @@ -334,7 +345,10 @@ private static Integer gdprFromQueryStringParams(HttpRequestContext httpRequest) return null; } - private static GppSidExtraction gppSidFromQueryStringParams(HttpRequestContext httpRequest) { + private GppSidExtraction gppSidFromQueryStringParams(HttpRequestContext httpRequest, + boolean debugEnabled, + List debugWarnings) { + final String gppSidParam = httpRequest.getQueryParams().get(GPP_SID_PARAM); try { @@ -346,6 +360,9 @@ private static GppSidExtraction gppSidFromQueryStringParams(HttpRequestContext h return GppSidExtraction.success(gppSid); } catch (IllegalArgumentException e) { + if (debugEnabled) { + debugWarnings.add("Failed to parse gppSid: '%s'".formatted(gppSidParam)); + } return GppSidExtraction.failed(); } } @@ -392,9 +409,10 @@ private Future updateBidRequest(AuctionContext auctionContext) { .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) - .map(this::fillExplicitParameters) + .map(bidRequest -> fillExplicitParameters(bidRequest, account)) .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())) .compose(resolvedBidRequest -> ortb2RequestFactory.validateRequest( resolvedBidRequest, auctionContext.getHttpRequest(), @@ -443,10 +461,10 @@ private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidR * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext} * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters */ - private BidRequest fillExplicitParameters(BidRequest bidRequest) { + private BidRequest fillExplicitParameters(BidRequest bidRequest, Account account) { final List imps = bidRequest.getImp(); // Force HTTPS as AMP requires it, but pubs can forget to set it. - final Imp imp = imps.get(0); + final Imp imp = imps.getFirst(); final Integer secure = imp.getSecure(); final boolean setSecure = secure == null || secure != 1; @@ -477,9 +495,10 @@ private BidRequest fillExplicitParameters(BidRequest bidRequest) { || setDefaultCache) { result = bidRequest.toBuilder() - .imp(setSecure ? Collections.singletonList(imps.get(0).toBuilder().secure(1).build()) : imps) + .imp(setSecure ? Collections.singletonList(imps.getFirst().toBuilder().secure(1).build()) : imps) .ext(extRequest( bidRequest, + account, setDefaultTargeting, setDefaultCache)) .build(); @@ -499,7 +518,7 @@ private BidRequest overrideParameters(BidRequest bidRequest, HttpRequestContext ortbTypesResolver.normalizeTargeting(targetingNode, errors, referer); final Site updatedSite = overrideSite(bidRequest.getSite()); - final Imp updatedImp = overrideImp(bidRequest.getImp().get(0), httpRequest, targetingNode); + final Imp updatedImp = overrideImp(bidRequest.getImp().getFirst(), httpRequest, targetingNode); if (ObjectUtils.anyNotNull(updatedSite, updatedImp)) { return bidRequest.toBuilder() @@ -676,6 +695,7 @@ private static List parseMultiSizeParam(String ms) { * Creates updated bidrequest.ext {@link ObjectNode}. */ private ExtRequest extRequest(BidRequest bidRequest, + Account account, boolean setDefaultTargeting, boolean setDefaultCache) { @@ -688,7 +708,7 @@ private ExtRequest extRequest(BidRequest bidRequest, : ExtRequestPrebid.builder(); if (setDefaultTargeting) { - prebidBuilder.targeting(createTargetingWithDefaults(prebid)); + prebidBuilder.targeting(createTargetingWithDefaults(prebid, account)); } if (setDefaultCache) { prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null), @@ -711,15 +731,14 @@ private ExtRequest extRequest(BidRequest bidRequest, * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's * child properties is missed or entire targeting does not exist. */ - private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) { + private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNull = targeting == null; final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity(); final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull(); - final JsonNode outgoingPriceGranularityNode - = isPriceGranularityNull - ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)) + final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull + ? mapper.mapper().valueToTree(ExtPriceGranularity.from(getDefaultPriceGranularity(account))) : priceGranularityNode; final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull @@ -743,6 +762,14 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) .build(); } + private static PriceGranularity getDefaultPriceGranularity(Account account) { + return Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + } + @Value(staticConstructor = "of") private static class GppSidExtraction { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 128e4b2630c..bd720a25f2b 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -7,6 +7,7 @@ import io.vertx.core.Future; import io.vertx.ext.web.RoutingContext; import org.prebid.server.auction.DebugResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.OrtbTypesResolver; @@ -48,6 +49,7 @@ public class AuctionRequestFactory { private final DebugResolver debugResolver; private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); @@ -63,7 +65,8 @@ public AuctionRequestFactory(long maxRequestSize, OrtbTypesResolver ortbTypesResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); @@ -78,12 +81,13 @@ public AuctionRequestFactory(long maxRequestSize, this.auctionPrivacyContextFactory = Objects.requireNonNull(auctionPrivacyContextFactory); this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); } /** - * Creates {@link AuctionContext} based on {@link RoutingContext}. + * Creates {@link AuctionContext} and parses BidRequest based on {@link RoutingContext}. */ - public Future fromRequest(RoutingContext routingContext, long startTime) { + public Future parseRequest(RoutingContext routingContext, long startTime) { final String body; try { body = extractAndValidateBody(routingContext); @@ -96,16 +100,30 @@ public Future fromRequest(RoutingContext routingContext, long st return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) .compose(httpRequest -> parseBidRequest(httpRequest, initialAuctionContext.getPrebidErrors()) - .map(bidRequest -> ortb2RequestFactory .enrichAuctionContext(initialAuctionContext, httpRequest, bidRequest, startTime) .with(requestTypeMetric(bidRequest)))) + .recover(ortb2RequestFactory::restoreResultFromRejection); + } - .compose(auctionContext -> ortb2RequestFactory.fetchAccount(auctionContext) - .map(auctionContext::with)) + /** + * Enriches {@link AuctionContext}. + */ + public Future enrichAuctionContext(AuctionContext initialContext) { + if (initialContext.isRequestRejected()) { + return Future.succeededFuture(initialContext); + } + + return ortb2RequestFactory.fetchAccount(initialContext).map(initialContext::with) .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + .compose(auctionContext -> gppService.contextFrom(auctionContext) .map(auctionContext::with)) @@ -121,17 +139,13 @@ public Future fromRequest(RoutingContext routingContext, long st .compose(auctionContext -> auctionPrivacyContextFactory.contextFrom(auctionContext) .map(auctionContext::with)) - .map(auctionContext -> auctionContext.with( - ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext))) + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) + .map(auctionContext::with)) .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) - .compose(ortb2RequestFactory::populateUserAdditionalInfo) - - .map(ortb2RequestFactory::enrichWithPriceFloors) - - .map(auctionContext -> ortb2RequestFactory.updateTimeout(auctionContext, startTime)) + .map(ortb2RequestFactory::updateTimeout) .recover(ortb2RequestFactory::restoreResultFromRejection); } @@ -216,14 +230,12 @@ private Regs fillRegsWithValuesFromHttpRequest(Regs regs, HttpRequestContext htt */ private Future updateAndValidateBidRequest(AuctionContext auctionContext) { final Account account = auctionContext.getAccount(); + final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); final List debugWarnings = auctionContext.getDebugWarnings(); return storedRequestProcessor.processAuctionRequest(account.getId(), auctionContext.getBidRequest()) .compose(auctionStoredResult -> updateBidRequest(auctionStoredResult, auctionContext)) - .compose(bidRequest -> ortb2RequestFactory.validateRequest( - bidRequest, - auctionContext.getHttpRequest(), - debugWarnings)) + .compose(bidRequest -> ortb2RequestFactory.validateRequest(bidRequest, httpRequest, debugWarnings)) .map(interstitialProcessor::process); } @@ -236,7 +248,8 @@ private Future updateBidRequest(AuctionStoredResult auctionStoredRes .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, hasStoredBidRequest)) - .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)); + .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())); } private static MetricName requestTypeMetric(BidRequest bidRequest) { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java index 9ffd8304caf..5bcabe413db 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -15,8 +15,6 @@ import com.iab.openrtb.request.Source; import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.User; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import lombok.Value; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.SetUtils; @@ -26,6 +24,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.PriceGranularity; @@ -35,12 +34,15 @@ import org.prebid.server.auction.model.Endpoint; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.auction.model.SecBrowsingTopic; -import org.prebid.server.exception.BlacklistedAppException; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.exception.BlocklistedAppException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; @@ -54,6 +56,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -91,8 +95,9 @@ public class Ortb2ImplicitParametersResolver { private final boolean shouldCacheOnlyWinningBids; private final boolean generateBidRequestId; private final String adServerCurrency; - private final List blacklistedApps; + private final List blocklistedApps; private final ExtRequestPrebidServer serverInfo; + private final BidderCatalog bidderCatalog; private final ImplicitParametersExtractor paramsExtractor; private final TimeoutResolver timeoutResolver; private final IpAddressHelper ipAddressHelper; @@ -104,10 +109,11 @@ public class Ortb2ImplicitParametersResolver { public Ortb2ImplicitParametersResolver(boolean shouldCacheOnlyWinningBids, boolean generateBidRequestId, String adServerCurrency, - List blacklistedApps, + List blocklistedApps, String externalUrl, Integer hostVendorId, String datacenterRegion, + BidderCatalog bidderCatalog, ImplicitParametersExtractor paramsExtractor, TimeoutResolver timeoutResolver, IpAddressHelper ipAddressHelper, @@ -119,8 +125,9 @@ public Ortb2ImplicitParametersResolver(boolean shouldCacheOnlyWinningBids, this.shouldCacheOnlyWinningBids = shouldCacheOnlyWinningBids; this.generateBidRequestId = generateBidRequestId; this.adServerCurrency = validateCurrency(Objects.requireNonNull(adServerCurrency)); - this.blacklistedApps = Objects.requireNonNull(blacklistedApps); + this.blocklistedApps = Objects.requireNonNull(blocklistedApps); this.serverInfo = ExtRequestPrebidServer.of(externalUrl, hostVendorId, datacenterRegion, null); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.paramsExtractor = Objects.requireNonNull(paramsExtractor); this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); @@ -153,7 +160,7 @@ public BidRequest resolve(BidRequest bidRequest, String endpoint, boolean hasStoredBidRequest) { - checkBlacklistedApp(bidRequest); + checkBlocklistedApp(bidRequest); final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); @@ -182,7 +189,11 @@ public BidRequest resolve(BidRequest bidRequest, final ExtRequest ext = bidRequest.getExt(); final List imps = bidRequest.getImp(); final ExtRequest populatedExt = populateRequestExt( - ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps), endpoint); + ext, + bidRequest, + ObjectUtils.defaultIfNull(populatedImps, imps), + endpoint, + auctionContext.getAccount()); final Source source = bidRequest.getSource(); final Source populatedSource = populateSource(source, populatedExt, hasStoredBidRequest); @@ -211,12 +222,12 @@ public static boolean isImpExtBidder(String field) { return !IMP_EXT_NON_BIDDER_FIELDS.contains(field); } - private void checkBlacklistedApp(BidRequest bidRequest) { + private void checkBlocklistedApp(BidRequest bidRequest) { final App app = bidRequest.getApp(); final String appId = app != null ? app.getId() : null; - if (StringUtils.isNotBlank(appId) && blacklistedApps.contains(appId)) { - throw new BlacklistedAppException( + if (StringUtils.isNotBlank(appId) && blocklistedApps.contains(appId)) { + throw new BlocklistedAppException( "Prebid-server does not process requests from App ID: " + appId); } } @@ -284,7 +295,7 @@ private String sanitizeIp(String ip, IpAddress.IP version) { return ipAddress != null && ipAddress.getVersion() == version ? ipAddress.getIp() : null; } - private IpAddress findIpFromRequest(HttpRequestContext request) { + public IpAddress findIpFromRequest(HttpRequestContext request) { final CaseInsensitiveMultiMap headers = request.getHeaders(); final String remoteHost = request.getRemoteHost(); final List requestIps = paramsExtractor.ipFrom(headers, remoteHost); @@ -440,7 +451,7 @@ private String getDomainOrNull(String url) { try { return paramsExtractor.domainFrom(url); } catch (PreBidException e) { - logger.warn("Error occurred while populating bid request: {0}", e.getMessage()); + logger.warn("Error occurred while populating bid request: {}", e.getMessage()); return null; } } @@ -638,6 +649,8 @@ private List populateImps(BidRequest bidRequest, return null; } + final BidderAliases aliases = aliases(bidRequest); + final ObjectNode globalBidderParams = extractGlobalBidderParams(bidRequest); final boolean isUniqueIds = isUniqueIds(imps); @@ -645,6 +658,7 @@ private List populateImps(BidRequest bidRequest, .range(0, imps.size()) .mapToObj(index -> new ImpPopulationContext( imps.get(index), + aliases, globalBidderParams, generateBidRequestId, hasStoredBidRequest, @@ -666,6 +680,14 @@ private List populateImps(BidRequest bidRequest, .toList(); } + private BidderAliases aliases(BidRequest bidRequest) { + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid prebid = extRequest != null ? extRequest.getPrebid() : null; + final Map aliases = prebid != null ? prebid.getAliases() : null; + final Map aliasgvlids = prebid != null ? prebid.getAliasgvlids() : null; + return BidderAliases.of(aliases, aliasgvlids, bidderCatalog); + } + private static ObjectNode extractGlobalBidderParams(BidRequest bidRequest) { final ExtRequest extRequest = bidRequest.getExt(); final ExtRequestPrebid extBidPrebid = extRequest != null ? extRequest.getPrebid() : null; @@ -697,10 +719,15 @@ private static boolean isUniqueIds(List imps) { return impIdsSet.size() == impIdsList.size(); } - private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List imps, String endpoint) { + private ExtRequest populateRequestExt(ExtRequest ext, + BidRequest bidRequest, + List imps, + String endpoint, + Account account) { + final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(ext, ExtRequest::getPrebid); - final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps); + final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps, account); final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest, endpoint); @@ -767,7 +794,7 @@ private static void resolveImpMediaTypes(Imp imp, Set impsMediaTypes) { /** * Returns populated {@link ExtRequestTargeting} or null if no changes were applied. */ - private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps) { + private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNotNull = targeting != null; @@ -780,8 +807,12 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List i if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) { return targeting.toBuilder() - .pricegranularity(resolvePriceGranularity(targeting, isPriceGranularityNull, - isPriceGranularityTextual, imps)) + .pricegranularity(resolvePriceGranularity( + targeting, + isPriceGranularityNull, + isPriceGranularityTextual, + imps, + account)) .includewinners(isIncludeWinnersNull || targeting.getIncludewinners()) .includebidderkeys(isIncludeBidderKeysNull ? !isWinningOnly(prebid.getCache()) @@ -806,14 +837,22 @@ private boolean isWinningOnly(ExtRequestPrebidCache cache) { * In case of valid string price granularity replaced it with appropriate custom view. * In case of invalid string value throws {@link InvalidRequestException}. */ - private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, - boolean isPriceGranularityTextual, List imps) { + private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, + boolean isPriceGranularityNull, + boolean isPriceGranularityTextual, + List imps, + Account account) { final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) .containsAll(getImpMediaTypes(imps)); if (isPriceGranularityNull && !hasAllMediaTypes) { - return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); + final PriceGranularity defaultPriceGranularity = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + return mapper.mapper().valueToTree(ExtPriceGranularity.from(defaultPriceGranularity)); } final JsonNode priceGranularityNode = targeting.getPricegranularity(); @@ -925,7 +964,7 @@ private Long resolveTmax(Long requestTimeout) { } @Value - private static class ImpPopulationContext { + private class ImpPopulationContext { private static final String DEALS_ONLY = "dealsonly"; private static final String PG_DEALS_ONLY = "pgdealsonly"; @@ -936,6 +975,7 @@ private static class ImpPopulationContext { Imp populatedImp; ImpPopulationContext(Imp imp, + BidderAliases aliases, ObjectNode globalBidderParams, boolean generateBidRequestId, boolean hasStoredBidRequest, @@ -947,6 +987,7 @@ private static class ImpPopulationContext { this.imp = imp; populatedImp = populateImp( imp, + aliases, globalBidderParams, generateBidRequestId, hasStoredBidRequest, @@ -960,14 +1001,15 @@ public Imp getPopulationResult() { return populatedImp != null ? populatedImp : imp; } - private static Imp populateImp(Imp imp, - ObjectNode globalBidderParams, - boolean generateBidRequestId, - boolean hasStoredBidRequest, - String impIdOverride, - JacksonMapper mapper, - IdGenerator tidGenerator, - JsonMerger jsonMerger) { + private Imp populateImp(Imp imp, + BidderAliases aliases, + ObjectNode globalBidderParams, + boolean generateBidRequestId, + boolean hasStoredBidRequest, + String impIdOverride, + JacksonMapper mapper, + IdGenerator tidGenerator, + JsonMerger jsonMerger) { final String impId = imp.getId(); final String populatedImpId = populateImpId(impId, impIdOverride); @@ -978,6 +1020,7 @@ private static Imp populateImp(Imp imp, final ObjectNode impExt = imp.getExt(); final ObjectNode populatedImpExt = populateImpExt( impExt, + aliases, globalBidderParams, generateBidRequestId, hasStoredBidRequest, @@ -1011,16 +1054,17 @@ private static Integer populateImpSecure(Integer impSecure) { return impSecure == null ? 1 : null; } - private static ObjectNode populateImpExt(ObjectNode impExt, - ObjectNode globalBidderParams, - boolean generateBidRequestId, - boolean hasStoredBidRequest, - JacksonMapper mapper, - IdGenerator tidGenerator, - JsonMerger jsonMerger) { + private ObjectNode populateImpExt(ObjectNode impExt, + BidderAliases aliases, + ObjectNode globalBidderParams, + boolean generateBidRequestId, + boolean hasStoredBidRequest, + JacksonMapper mapper, + IdGenerator tidGenerator, + JsonMerger jsonMerger) { final ObjectNode modifiedImpExt = prepareValidImpExtCopy(impExt, mapper); - final boolean isMoved = moveBidderParamsToPrebid(modifiedImpExt); + final boolean isMoved = moveBidderParamsToPrebid(modifiedImpExt, aliases); final boolean isMerged = mergeGlobalBidderParamsToImpExt(modifiedImpExt, globalBidderParams, jsonMerger); final boolean isDealsOnlyModified = modifyDealsOnly(modifiedImpExt); final boolean isNonBidderFieldsModified = modifyNonBidderFields( @@ -1048,8 +1092,11 @@ private static ObjectNode getOrCreateChildObjectNode(ObjectNode parentNode, Stri return isObjectNode(childNode) ? (ObjectNode) childNode : parentNode.putObject(fieldName); } - private static boolean moveBidderParamsToPrebid(ObjectNode impExt) { + private boolean moveBidderParamsToPrebid(ObjectNode impExt, BidderAliases aliases) { final ObjectNode extPrebidBidder = bidderParamsFromImpExt(impExt); + if (!extPrebidBidder.isEmpty()) { + return false; + } final Set bidders = StreamUtil.asStream(impExt.fieldNames()) .filter(Ortb2ImplicitParametersResolver::isImpExtBidder) @@ -1059,16 +1106,22 @@ private static boolean moveBidderParamsToPrebid(ObjectNode impExt) { return false; } + boolean modified = false; for (String bidder : bidders) { - final ObjectNode bidderNode = getOrCreateChildObjectNode(extPrebidBidder, bidder); + final JsonNode impExtBidderNode = impExt.get(bidder); + if (!isObjectNode(impExtBidderNode)) { + continue; + } - final JsonNode impExtBidderNode = impExt.remove(bidder); - if (isObjectNode(impExtBidderNode)) { - bidderNode.setAll((ObjectNode) impExtBidderNode); + final ObjectNode bidderNode = getOrCreateChildObjectNode(extPrebidBidder, bidder); + bidderNode.setAll((ObjectNode) impExtBidderNode); + if (bidderCatalog.isValidName(aliases.resolveBidder(bidder))) { + impExt.remove(bidder); } + modified = true; } - return true; + return modified; } private static ObjectNode bidderParamsFromImpExt(ObjectNode ext) { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 67bd1dd38b7..336f2b5f8f1 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -4,15 +4,17 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; +import lombok.Getter; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -27,16 +29,12 @@ import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.deals.UserAdditionalInfoService; -import org.prebid.server.deals.model.DeepDebugLog; -import org.prebid.server.deals.model.TxnLog; -import org.prebid.server.exception.BlacklistedAccountException; +import org.prebid.server.exception.BlocklistedAccountException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; @@ -45,6 +43,8 @@ import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -53,15 +53,14 @@ import org.prebid.server.model.UpdateResult; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; -import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; -import org.prebid.server.proto.openrtb.ext.request.TraceLevel; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; @@ -75,14 +74,15 @@ import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.model.ValidationResult; -import java.time.Clock; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.TreeMap; import java.util.function.Function; +import java.util.stream.Stream; public class Ortb2RequestFactory { @@ -91,10 +91,9 @@ public class Ortb2RequestFactory { private static final ConditionalLogger EMPTY_ACCOUNT_LOGGER = new ConditionalLogger("empty_account", logger); private static final ConditionalLogger UNKNOWN_ACCOUNT_LOGGER = new ConditionalLogger("unknown_account", logger); - private final boolean enforceValidAccount; private final int timeoutAdjustmentFactor; private final double logSamplingRate; - private final List blacklistedAccounts; + private final List blocklistedAccounts; private final UidsCookieService uidsCookieService; private final ActivityInfrastructureCreator activityInfrastructureCreator; private final RequestValidator requestValidator; @@ -102,18 +101,14 @@ public class Ortb2RequestFactory { private final TimeoutFactory timeoutFactory; private final StoredRequestProcessor storedRequestProcessor; private final ApplicationSettings applicationSettings; - private final UserAdditionalInfoService userAdditionalInfoService; private final IpAddressHelper ipAddressHelper; private final HookStageExecutor hookStageExecutor; - private final PriceFloorProcessor priceFloorProcessor; private final CountryCodeMapper countryCodeMapper; private final Metrics metrics; - private final Clock clock; - public Ortb2RequestFactory(boolean enforceValidAccount, - int timeoutAdjustmentFactor, + public Ortb2RequestFactory(int timeoutAdjustmentFactor, double logSamplingRate, - List blacklistedAccounts, + List blocklistedAccounts, UidsCookieService uidsCookieService, ActivityInfrastructureCreator activityInfrastructureCreator, RequestValidator requestValidator, @@ -123,20 +118,16 @@ public Ortb2RequestFactory(boolean enforceValidAccount, ApplicationSettings applicationSettings, IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, - UserAdditionalInfoService userAdditionalInfoService, - PriceFloorProcessor priceFloorProcessor, CountryCodeMapper countryCodeMapper, - Metrics metrics, - Clock clock) { + Metrics metrics) { if (timeoutAdjustmentFactor < 0 || timeoutAdjustmentFactor > 100) { throw new IllegalArgumentException("Expected timeout adjustment factor should be in [0, 100]."); } - this.enforceValidAccount = enforceValidAccount; this.timeoutAdjustmentFactor = timeoutAdjustmentFactor; this.logSamplingRate = logSamplingRate; - this.blacklistedAccounts = Objects.requireNonNull(blacklistedAccounts); + this.blocklistedAccounts = Objects.requireNonNull(blocklistedAccounts); this.uidsCookieService = Objects.requireNonNull(uidsCookieService); this.activityInfrastructureCreator = Objects.requireNonNull(activityInfrastructureCreator); this.requestValidator = Objects.requireNonNull(requestValidator); @@ -146,11 +137,8 @@ public Ortb2RequestFactory(boolean enforceValidAccount, this.applicationSettings = Objects.requireNonNull(applicationSettings); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); - this.userAdditionalInfoService = userAdditionalInfoService; - this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); this.countryCodeMapper = Objects.requireNonNull(countryCodeMapper); this.metrics = Objects.requireNonNull(metrics); - this.clock = Objects.requireNonNull(clock); } public AuctionContext createAuctionContext(Endpoint endpoint, MetricName requestTypeMetric) { @@ -161,7 +149,6 @@ public AuctionContext createAuctionContext(Endpoint endpoint, MetricName request .hookExecutionContext(HookExecutionContext.of(endpoint)) .debugContext(DebugContext.empty()) .requestRejected(false) - .txnLog(TxnLog.create()) .debugHttpCalls(new HashMap<>()) .bidRejectionTrackers(new TreeMap<>(String.CASE_INSENSITIVE_ORDER)) .build(); @@ -177,7 +164,6 @@ public AuctionContext enrichAuctionContext(AuctionContext auctionContext, .uidsCookie(uidsCookieService.parseFromRequest(httpRequest)) .bidRequest(bidRequest) .timeoutContext(TimeoutContext.of(startTime, timeout(bidRequest, startTime), timeoutAdjustmentFactor)) - .deepDebugLog(createDeepDebugLog(bidRequest)) .build(); } @@ -195,7 +181,7 @@ private Future fetchAccount(AuctionContext auctionContext, boolean isLo final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); return findAccountIdFrom(bidRequest, isLookupStoredRequest) - .map(this::validateIfAccountBlacklisted) + .map(this::validateIfAccountBlocklisted) .compose(accountId -> loadAccount(timeout, httpRequest, accountId)); } @@ -221,7 +207,70 @@ public Future validateRequest(BidRequest bidRequest, : Future.succeededFuture(bidRequest); } - public BidRequest enrichBidRequestWithAccountAndPrivacyData(AuctionContext auctionContext) { + public BidRequest removeEmptyEids(BidRequest bidRequest, List warnings) { + final User user = bidRequest.getUser(); + + if (user == null) { + return bidRequest; + } + + final List eids = Stream.ofNullable(user.getEids()) + .flatMap(Collection::stream) + .map(eid -> eid.toBuilder().uids(removeEmptyUids(eid, warnings)).build()) + .filter(eid -> CollectionUtils.isNotEmpty(eid.getUids())) + .toList(); + + if (CollectionUtils.isEmpty(eids) && CollectionUtils.isNotEmpty(user.getEids())) { + warnings.add("removed empty EID array"); + } + + final User modifiedUser = user.toBuilder().eids(CollectionUtils.isEmpty(eids) ? null : eids).build(); + return bidRequest.toBuilder().user(modifiedUser).build(); + } + + private List removeEmptyUids(Eid eid, List warnings) { + return CollectionUtils.emptyIfNull(eid.getUids()).stream() + .filter(uid -> { + if (StringUtils.isBlank(uid.getId())) { + warnings.add("removed EID %s due to empty ID".formatted(eid.getSource())); + return false; + } + + return true; + }) + .toList(); + } + + public Future enrichBidRequestWithGeolocationData(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Device device = bidRequest.getDevice(); + final GeoInfo geoInfo = auctionContext.getGeoInfo(); + final Geo geo = ObjectUtil.getIfNotNull(device, Device::getGeo); + + final UpdateResult resolvedCountry = resolveCountry(geo, geoInfo); + final UpdateResult resolvedRegion = resolveRegion(geo, geoInfo); + + if (!resolvedCountry.isUpdated() && !resolvedRegion.isUpdated()) { + return Future.succeededFuture(bidRequest); + } + + final Geo updatedGeo = Optional.ofNullable(geo) + .map(Geo::toBuilder) + .orElseGet(Geo::builder) + .country(resolvedCountry.getValue()) + .region(resolvedRegion.getValue()) + .build(); + + final Device updatedDevice = Optional.ofNullable(device) + .map(Device::toBuilder) + .orElseGet(Device::builder) + .geo(updatedGeo) + .build(); + + return Future.succeededFuture(bidRequest.toBuilder().device(updatedDevice).build()); + } + + public Future enrichBidRequestWithAccountAndPrivacyData(AuctionContext auctionContext) { final BidRequest bidRequest = auctionContext.getBidRequest(); final Account account = auctionContext.getAccount(); final PrivacyContext privacyContext = auctionContext.getPrivacyContext(); @@ -235,15 +284,15 @@ public BidRequest enrichBidRequestWithAccountAndPrivacyData(AuctionContext aucti final Regs regs = bidRequest.getRegs(); final Regs enrichedRegs = enrichRegs(regs, privacyContext, account); - if (enrichedRequestExt != null || enrichedDevice != null || enrichedRegs != null) { - return bidRequest.toBuilder() - .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) - .device(ObjectUtils.defaultIfNull(enrichedDevice, device)) - .regs(ObjectUtils.defaultIfNull(enrichedRegs, regs)) - .build(); + if (enrichedRequestExt == null && enrichedDevice == null && enrichedRegs == null) { + return Future.succeededFuture(bidRequest); } - return bidRequest; + return Future.succeededFuture(bidRequest.toBuilder() + .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) + .device(ObjectUtils.defaultIfNull(enrichedDevice, device)) + .regs(ObjectUtils.defaultIfNull(enrichedRegs, regs)) + .build()); } private static Regs enrichRegs(Regs regs, PrivacyContext privacyContext, Account account) { @@ -275,9 +324,9 @@ private static Regs enrichRegs(Regs regs, PrivacyContext privacyContext, Account } private static ExtRegs mapRegsExtDsa(DefaultDsa defaultDsa, ExtRegs regsExt) { - final List enrichedDsaTransparencies = defaultDsa.getTransparency() + final List enrichedDsaTransparencies = defaultDsa.getTransparency() .stream() - .map(dsaTransparency -> ExtRegsDsaTransparency.of( + .map(dsaTransparency -> DsaTransparency.of( dsaTransparency.getDomain(), dsaTransparency.getDsaParams())) .toList(); @@ -355,18 +404,9 @@ private static BidRequest toBidRequest(HookStageExecutionResult populateUserAdditionalInfo(AuctionContext auctionContext) { - return userAdditionalInfoService != null - ? userAdditionalInfoService.populate(auctionContext) - : Future.succeededFuture(auctionContext); - } - - public AuctionContext enrichWithPriceFloors(AuctionContext auctionContext) { - return priceFloorProcessor.enrichWithPriceFloors(auctionContext); - } - - public AuctionContext updateTimeout(AuctionContext auctionContext, long startTime) { + public AuctionContext updateTimeout(AuctionContext auctionContext) { final TimeoutContext timeoutContext = auctionContext.getTimeoutContext(); + final long startTime = timeoutContext.getStartTime(); final Timeout currentTimeout = timeoutContext.getTimeout(); final BidRequest bidRequest = auctionContext.getBidRequest(); @@ -414,32 +454,38 @@ private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookup .map(storedAuctionResult -> accountIdFrom(storedAuctionResult.bidRequest())); } - private String validateIfAccountBlacklisted(String accountId) { - if (CollectionUtils.isNotEmpty(blacklistedAccounts) + private String validateIfAccountBlocklisted(String accountId) { + if (CollectionUtils.isNotEmpty(blocklistedAccounts) && StringUtils.isNotBlank(accountId) - && blacklistedAccounts.contains(accountId)) { + && blocklistedAccounts.contains(accountId)) { - throw new BlacklistedAccountException( - "Prebid-server has blacklisted Account ID: %s, please reach out to the prebid server host." + throw new BlocklistedAccountException( + "Prebid-server has blocklisted Account ID: %s, please reach out to the prebid server host." .formatted(accountId)); } return accountId; } - private Future loadAccount(Timeout timeout, - HttpRequestContext httpRequest, - String accountId) { - - final Future accountFuture = StringUtils.isBlank(accountId) - ? responseForEmptyAccount(httpRequest) - : applicationSettings.getAccountById(accountId, timeout) - .compose(this::ensureAccountActive, - exception -> accountFallback(exception, accountId, httpRequest)); + private Future loadAccount(Timeout timeout, HttpRequestContext httpRequest, String accountId) { + if (StringUtils.isBlank(accountId)) { + EMPTY_ACCOUNT_LOGGER.warn(accountErrorMessage("Account not specified", httpRequest), logSamplingRate); + } - return accountFuture + return applicationSettings.getAccountById(accountId, timeout) + .compose(this::ensureAccountActive) + .recover(exception -> wrapFailure(exception, accountId, httpRequest)) .onFailure(ignored -> metrics.updateAccountRequestRejectedByInvalidAccountMetrics(accountId)); } + private Future ensureAccountActive(Account account) { + final String accountId = account.getId(); + + return account.getStatus() == AccountStatus.inactive + ? Future.failedFuture( + new UnauthorizedAccountException("Account %s is inactive".formatted(accountId), accountId)) + : Future.succeededFuture(account); + } + /** * Extracts publisher id either from {@link BidRequest}.app.publisher or {@link BidRequest}.site.publisher. * If neither is present returns empty string. @@ -474,48 +520,26 @@ private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; } - private Future responseForEmptyAccount(HttpRequestContext httpRequest) { - EMPTY_ACCOUNT_LOGGER.warn(accountErrorMessage("Account not specified", httpRequest), logSamplingRate); - return responseForUnknownAccount(StringUtils.EMPTY); - } - - private static String accountErrorMessage(String message, HttpRequestContext httpRequest) { - return "%s, Url: %s and Referer: %s".formatted( - message, - httpRequest.getAbsoluteUri(), - httpRequest.getHeaders().get(HttpUtil.REFERER_HEADER)); - } - - private Future accountFallback(Throwable exception, - String accountId, - HttpRequestContext httpRequest) { - - if (exception instanceof PreBidException) { + private Future wrapFailure(Throwable exception, String accountId, HttpRequestContext httpRequest) { + if (exception instanceof UnauthorizedAccountException) { + return Future.failedFuture(exception); + } else if (exception instanceof PreBidException) { UNKNOWN_ACCOUNT_LOGGER.warn(accountErrorMessage(exception.getMessage(), httpRequest), 100); } else { metrics.updateAccountRequestRejectedByFailedFetch(accountId); - logger.warn("Error occurred while fetching account: {0}", exception.getMessage()); + logger.warn("Error occurred while fetching account: {}", exception.getMessage()); logger.debug("Error occurred while fetching account", exception); } - // hide all errors occurred while fetching account - return responseForUnknownAccount(accountId); + return Future.failedFuture( + new UnauthorizedAccountException("Unauthorized account id: " + accountId, accountId)); } - private Future responseForUnknownAccount(String accountId) { - return enforceValidAccount - ? Future.failedFuture(new UnauthorizedAccountException( - "Unauthorized account id: " + accountId, accountId)) - : Future.succeededFuture(Account.empty(accountId)); - } - - private Future ensureAccountActive(Account account) { - final String accountId = account.getId(); - - return account.getStatus() == AccountStatus.inactive - ? Future.failedFuture(new UnauthorizedAccountException( - "Account %s is inactive".formatted(accountId), accountId)) - : Future.succeededFuture(account); + private static String accountErrorMessage(String message, HttpRequestContext httpRequest) { + return "%s, Url: %s and Referer: %s".formatted( + message, + httpRequest.getAbsoluteUri(), + httpRequest.getHeaders().get(HttpUtil.REFERER_HEADER)); } private ExtRequest enrichExtRequest(ExtRequest ext, Account account) { @@ -613,9 +637,10 @@ private Device enrichDevice(Device device, PrivacyContext privacyContext) { final boolean shouldUpdateIpV6 = ipV6 != null && !Objects.equals(ipV6InRequest, ipV6); final Geo geo = ObjectUtil.getIfNotNull(device, Device::getGeo); + final GeoInfo geoInfo = privacyContext.getTcfContext().getGeoInfo(); - final UpdateResult resolvedCountry = resolveCountry(geo, privacyContext); - final UpdateResult resolvedRegion = resolveRegion(geo, privacyContext); + final UpdateResult resolvedCountry = resolveCountry(geo, geoInfo); + final UpdateResult resolvedRegion = resolveRegion(geo, geoInfo); if (shouldUpdateIpV4 || shouldUpdateIpV6 || resolvedCountry.isUpdated() || resolvedRegion.isUpdated()) { final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); @@ -645,10 +670,9 @@ private Device enrichDevice(Device device, PrivacyContext privacyContext) { return null; } - private UpdateResult resolveCountry(Geo geo, PrivacyContext privacyContext) { - final String countryInRequest = geo != null ? geo.getCountry() : null; + private UpdateResult resolveCountry(Geo originalGeo, GeoInfo geoInfo) { + final String countryInRequest = originalGeo != null ? originalGeo.getCountry() : null; - final GeoInfo geoInfo = privacyContext.getTcfContext().getGeoInfo(); final String alpha2CountryCode = geoInfo != null ? geoInfo.getCountry() : null; final String alpha3CountryCode = countryCodeMapper.mapToAlpha3(alpha2CountryCode); @@ -657,11 +681,10 @@ private UpdateResult resolveCountry(Geo geo, PrivacyContext privacyConte : UpdateResult.unaltered(countryInRequest); } - private static UpdateResult resolveRegion(Geo geo, PrivacyContext privacyContext) { - final String regionInRequest = geo != null ? geo.getRegion() : null; + private static UpdateResult resolveRegion(Geo originalGeo, GeoInfo geoInfo) { + final String regionInRequest = originalGeo != null ? originalGeo.getRegion() : null; final String upperCasedRegionInRequest = StringUtils.upperCase(regionInRequest); - final GeoInfo geoInfo = privacyContext.getTcfContext().getGeoInfo(); final String region = geoInfo != null ? geoInfo.getRegion() : null; final String upperCasedRegion = StringUtils.upperCase(region); @@ -679,19 +702,7 @@ private static CaseInsensitiveMultiMap toCaseInsensitiveMultiMap(MultiMap origin return mapBuilder.build(); } - private DeepDebugLog createDeepDebugLog(BidRequest bidRequest) { - final ExtRequest ext = bidRequest.getExt(); - return DeepDebugLog.create(ext != null && isDeepDebugEnabled(ext), clock); - } - - /** - * Determines deep debug flag from {@link ExtRequest}. - */ - private static boolean isDeepDebugEnabled(ExtRequest extRequest) { - final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; - return extRequestPrebid != null && extRequestPrebid.getTrace() == TraceLevel.verbose; - } - + @Getter static class RejectedRequestException extends RuntimeException { private final AuctionContext auctionContext; @@ -699,10 +710,6 @@ static class RejectedRequestException extends RuntimeException { RejectedRequestException(AuctionContext auctionContext) { this.auctionContext = auctionContext; } - - public AuctionContext getAuctionContext() { - return auctionContext; - } } private record TargetingValueResolver(ExtRequestTargeting targeting, diff --git a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java index 07c256b216a..8c4d5de8614 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java @@ -16,12 +16,13 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.DebugResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.CachedDebugLog; import org.prebid.server.auction.model.WithPodErrors; -import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.DecodeException; @@ -60,6 +61,7 @@ public class VideoRequestFactory { private final AuctionPrivacyContextFactory auctionPrivacyContextFactory; private final DebugResolver debugResolver; private final JacksonMapper mapper; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; public VideoRequestFactory(int maxRequestSize, boolean enforceStoredRequest, @@ -70,7 +72,8 @@ public VideoRequestFactory(int maxRequestSize, Ortb2ImplicitParametersResolver paramsResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { this.enforceStoredRequest = enforceStoredRequest; this.maxRequestSize = maxRequestSize; @@ -81,6 +84,7 @@ public VideoRequestFactory(int maxRequestSize, this.auctionPrivacyContextFactory = Objects.requireNonNull(auctionPrivacyContextFactory); this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); this.escapeLogCacheRegexPattern = StringUtils.isNotBlank(escapeLogCacheRegex) ? Pattern.compile(escapeLogCacheRegex) @@ -104,42 +108,43 @@ public Future> fromRequest(RoutingContext routingC Endpoint.openrtb2_video, MetricName.video); return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) - .compose(httpRequest -> - createBidRequest(httpRequest) + .compose(httpRequest -> createBidRequest(httpRequest) + .map(bidRequest -> removeEmptyEids(bidRequest, initialAuctionContext.getDebugWarnings())) + .compose(bidRequest -> validateRequest( + bidRequest, + httpRequest, + initialAuctionContext.getDebugWarnings())) - .compose(bidRequest -> validateRequest( - bidRequest, - httpRequest, - initialAuctionContext.getDebugWarnings())) + .map(bidRequestWithErrors -> populatePodErrors( + bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - .map(bidRequestWithErrors -> populatePodErrors( - bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - - .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( - initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) + .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) .compose(auctionContext -> ortb2RequestFactory.fetchAccountWithoutStoredRequestLookup(auctionContext) .map(auctionContext::with)) .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + .compose(auctionContext -> ortb2RequestFactory.activityInfrastructureFrom(auctionContext) .map(auctionContext::with)) .compose(auctionContext -> auctionPrivacyContextFactory.contextFrom(auctionContext) .map(auctionContext::with)) - .map(auctionContext -> auctionContext.with( - ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext))) + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) + .map(auctionContext::with)) .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) - .compose(ortb2RequestFactory::populateUserAdditionalInfo) - - .map(ortb2RequestFactory::enrichWithPriceFloors) - - .map(auctionContext -> ortb2RequestFactory.updateTimeout(auctionContext, startTime)) + .map(ortb2RequestFactory::updateTimeout) .recover(ortb2RequestFactory::restoreResultFromRejection) @@ -148,6 +153,14 @@ public Future> fromRequest(RoutingContext routingC .map(auctionContext -> WithPodErrors.of(auctionContext, podErrors)); } + private WithPodErrors removeEmptyEids(WithPodErrors requestWithPodErrors, + List debugWarnings) { + + return WithPodErrors.of( + ortb2RequestFactory.removeEmptyEids(requestWithPodErrors.getData(), debugWarnings), + requestWithPodErrors.getPodErrors()); + } + private String extractAndValidateBody(RoutingContext routingContext) { final String body = routingContext.getBodyAsString(); if (body == null) { diff --git a/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java b/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java index e0198ac0d20..43de9dff9a0 100644 --- a/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java +++ b/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java @@ -2,23 +2,16 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Content; -import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Producer; -import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Source; import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverter; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; @@ -37,9 +30,6 @@ public class BidRequestOrtb26To25Converter implements BidRequestOrtbVersionConve private static final String PREBID_FIELD = "prebid"; private static final String IS_REWARDED_INVENTORY_FIELD = "is_rewarded_inventory"; - private static final Producer EMPTY_PRODUCER = Producer.builder().build(); - private static final Publisher EMPTY_PUBLISHER = Publisher.builder().build(); - private final JacksonMapper mapper; public BidRequestOrtb26To25Converter(JacksonMapper mapper) { @@ -51,15 +41,6 @@ public BidRequest convert(BidRequest bidRequest) { final List imps = bidRequest.getImp(); final List modifiedImps = modifyImps(imps); - final Site site = bidRequest.getSite(); - final Site modifiedSite = modifySite(site); - - final App app = bidRequest.getApp(); - final App modifiedApp = modifyApp(app); - - final Device device = bidRequest.getDevice(); - final Device modifiedDevice = modifyDevice(device); - final User user = bidRequest.getUser(); final User modifiedUser = modifyUser(user); @@ -71,25 +52,13 @@ public BidRequest convert(BidRequest bidRequest) { return ObjectUtils.anyNotNull( modifiedImps, - modifiedSite, - modifiedApp, - modifiedDevice, modifiedUser, - bidRequest.getWlangb(), - bidRequest.getCattax(), - bidRequest.getDooh(), modifiedSource, modifiedRegs) ? bidRequest.toBuilder() .imp(modifiedImps != null ? modifiedImps : imps) - .site(modifiedSite != null ? modifiedSite : site) - .app(modifiedApp != null ? modifiedApp : app) - .device(modifiedDevice != null ? modifiedDevice : device) .user(modifiedUser != null ? modifiedUser : user) - .wlangb(null) - .cattax(null) - .dooh(null) .source(modifiedSource != null ? modifiedSource : source) .regs(modifiedRegs != null ? modifiedRegs : regs) .build() @@ -112,90 +81,10 @@ private List modifyImps(List imps) { } private Imp modifyImp(Imp imp) { - final Video video = imp.getVideo(); - final Video modifiedVideo = modifyVideo(video); - - final Audio audio = imp.getAudio(); - final Audio modifiedAudio = modifyAudio(audio); - - final ObjectNode impExt = imp.getExt(); - final ObjectNode modifiedImpExt = modifyImpExt(impExt, imp.getRwdd()); - - return ObjectUtils.anyNotNull(modifiedVideo, - modifiedAudio, - imp.getSsai(), - imp.getQty(), - imp.getDt(), - imp.getRefresh(), - modifiedImpExt) - - ? imp.toBuilder() - .video(modifiedVideo != null ? modifiedVideo : video) - .audio(modifiedAudio != null ? modifiedAudio : audio) - .rwdd(null) - .ssai(null) - .qty(null) - .dt(null) - .refresh(null) - .ext(modifiedImpExt != null ? modifiedImpExt : impExt) - .build() - - : null; - } - - private static Video modifyVideo(Video video) { - if (video == null) { - return null; - } - - return ObjectUtils.anyNotNull( - video.getMaxseq(), - video.getPoddur(), - video.getPodid(), - video.getPodseq(), - video.getRqddurs(), - video.getSlotinpod(), - video.getMincpmpersec(), - video.getPlcmt()) - - ? video.toBuilder() - .maxseq(null) - .poddur(null) - .podid(null) - .podseq(null) - .rqddurs(null) - .slotinpod(null) - .mincpmpersec(null) - .plcmt(null) - .build() - - : null; - } - - private static Audio modifyAudio(Audio audio) { - if (audio == null) { - return null; - } - - return ObjectUtils.anyNotNull( - audio.getPoddur(), - audio.getRqddurs(), - audio.getPodid(), - audio.getPodseq(), - audio.getSlotinpod(), - audio.getMincpmpersec(), - audio.getMaxseq()) - - ? audio.toBuilder() - .poddur(null) - .rqddurs(null) - .podid(null) - .podseq(null) - .slotinpod(null) - .mincpmpersec(null) - .maxseq(null) - .build() + final ObjectNode modifiedImpExt = modifyImpExt(imp.getExt(), imp.getRwdd()); + return modifiedImpExt != null + ? imp.toBuilder().ext(modifiedImpExt).build() : null; } @@ -218,160 +107,25 @@ private ObjectNode modifyImpExt(ObjectNode impExt, Integer rewarded) { return copy; } - private static Site modifySite(Site site) { - if (site == null) { - return null; - } - - final Publisher publisher = site.getPublisher(); - final Publisher modifiedPublisher = modifyPublisher(publisher); - - final Content content = site.getContent(); - final Content modifiedContent = modifyContent(content); - - return ObjectUtils.anyNotNull( - site.getCattax(), - site.getInventorypartnerdomain(), - modifiedPublisher, - modifiedContent, - site.getKwarray()) - - ? site.toBuilder() - .cattax(null) - .inventorypartnerdomain(null) - .publisher(modifiedPublisher != null ? nullIfEmpty(modifiedPublisher) : publisher) - .content(modifiedContent != null ? nullIfEmpty(modifiedContent) : content) - .kwarray(null) - .build() - - : null; - } - - private static Publisher modifyPublisher(Publisher publisher) { - return publisher != null && publisher.getCattax() != null - ? publisher.toBuilder() - .cattax(null) - .build() - : null; - } - - private static Content modifyContent(Content content) { - if (content == null) { - return null; - } - - final Producer producer = content.getProducer(); - final Producer modifiedProducer = modifyProducer(producer); - - return ObjectUtils.anyNotNull( - modifiedProducer, - content.getCattax(), - content.getKwarray(), - content.getLangb(), - content.getNetwork(), - content.getChannel()) - - ? content.toBuilder() - .producer(modifiedProducer != null ? nullIfEmpty(modifiedProducer) : producer) - .cattax(null) - .kwarray(null) - .langb(null) - .network(null) - .channel(null) - .build() - - : null; - } - - private static Producer modifyProducer(Producer producer) { - return producer != null && producer.getCattax() != null - ? producer.toBuilder() - .cattax(null) - .build() - : null; - } - - private static Producer nullIfEmpty(Producer producer) { - return nullIfEmpty(producer, EMPTY_PRODUCER.equals(producer)); - } - - private static Publisher nullIfEmpty(Publisher publisher) { - return nullIfEmpty(publisher, EMPTY_PUBLISHER.equals(publisher)); - } - - private static Content nullIfEmpty(Content content) { - return nullIfEmpty(content, content.isEmpty()); - } - - private static T nullIfEmpty(T object, boolean isEmpty) { - return isEmpty ? null : object; - } - - private static App modifyApp(App app) { - if (app == null) { - return null; - } - - final Publisher publisher = app.getPublisher(); - final Publisher modifiedPublisher = modifyPublisher(publisher); - - final Content content = app.getContent(); - final Content modifiedContent = modifyContent(content); - - return ObjectUtils.anyNotNull( - app.getCattax(), - app.getInventorypartnerdomain(), - modifiedPublisher, - modifiedContent, - app.getKwarray()) - - ? app.toBuilder() - .cattax(null) - .inventorypartnerdomain(null) - .publisher(modifiedPublisher != null ? nullIfEmpty(modifiedPublisher) : publisher) - .content(modifiedContent != null ? nullIfEmpty(modifiedContent) : content) - .kwarray(null) - .build() - - : null; - } - - private static Device modifyDevice(Device device) { - if (device == null) { - return null; - } - - return ObjectUtils.anyNotNull(device.getSua(), device.getLangb()) - ? device.toBuilder() - .sua(null) - .langb(null) - .build() - : null; - } - private static User modifyUser(User user) { if (user == null) { return null; } - final ExtUser extUser = user.getExt(); - final ExtUser modifiedExtUser = modifyUserExt(extUser, user.getConsent(), user.getEids()); + final List eids = user.getEids(); + final String consent = user.getConsent(); + if (StringUtils.isEmpty(consent) && CollectionUtils.isEmpty(eids)) { + return null; + } - return ObjectUtils.anyNotNull(user.getKwarray(), modifiedExtUser) - ? user.toBuilder() - .kwarray(null) - .consent(null) + return user.toBuilder() .eids(null) - .ext(modifiedExtUser != null ? modifiedExtUser : extUser) - .build() - : null; + .consent(null) + .ext(modifyUserExt(user.getExt(), consent, eids)) + .build(); } private static ExtUser modifyUserExt(ExtUser extUser, String consent, List eids) { - if (consent == null && CollectionUtils.isEmpty(eids)) { - return null; - } - final ExtUser modifiedExtUser = Optional.ofNullable(extUser) .map(ExtUser::toBuilder) .orElseGet(ExtUser::builder) @@ -415,9 +169,7 @@ private static Regs modifyRegs(Regs regs) { final Integer gdpr = regs.getGdpr(); final String usPrivacy = regs.getUsPrivacy(); - final String gpp = regs.getGpp(); - final List gppSid = regs.getGppSid(); - if (gdpr == null && usPrivacy == null && gpp == null && gppSid == null) { + if (gdpr == null && usPrivacy == null) { return null; } @@ -430,8 +182,6 @@ private static Regs modifyRegs(Regs regs) { return regs.toBuilder() .gdpr(null) .usPrivacy(null) - .gpp(null) - .gppSid(null) .ext(extRegs) .build(); } diff --git a/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java b/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java index 38fa9ec8ede..0560ba01e77 100644 --- a/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java +++ b/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java @@ -1,14 +1,14 @@ package org.prebid.server.bidder; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.Objects; diff --git a/src/main/java/org/prebid/server/bidder/BidderInfo.java b/src/main/java/org/prebid/server/bidder/BidderInfo.java index fffbb1bab5b..c9659135eb7 100644 --- a/src/main/java/org/prebid/server/bidder/BidderInfo.java +++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java @@ -28,6 +28,8 @@ public class BidderInfo { List vendors; + List currencyAccepted; + GdprInfo gdpr; boolean ccpaEnforced; @@ -49,6 +51,7 @@ public static BidderInfo create(boolean enabled, List doohMediaTypes, List supportedVendors, int vendorId, + List currencyAccepted, boolean ccpaEnforced, boolean modifyingVastXmlAllowed, CompressionType compressionType, @@ -66,6 +69,7 @@ public static BidderInfo create(boolean enabled, platformInfo(siteMediaTypes), platformInfo(doohMediaTypes)), supportedVendors, + currencyAccepted, new GdprInfo(vendorId), ccpaEnforced, modifyingVastXmlAllowed, diff --git a/src/main/java/org/prebid/server/bidder/DealsBidderRequestCompletionTrackerFactory.java b/src/main/java/org/prebid/server/bidder/DealsBidderRequestCompletionTrackerFactory.java deleted file mode 100644 index 8be85f88415..00000000000 --- a/src/main/java/org/prebid/server/bidder/DealsBidderRequestCompletionTrackerFactory.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.prebid.server.bidder; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Deal; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Pmp; -import com.iab.openrtb.response.Bid; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.bidder.model.BidderBid; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class DealsBidderRequestCompletionTrackerFactory implements BidderRequestCompletionTrackerFactory { - - public BidderRequestCompletionTracker create(BidRequest bidRequest) { - final Map impToTopDealMap = new HashMap<>(); - for (final Imp imp : bidRequest.getImp()) { - final Pmp pmp = imp.getPmp(); - final List deals = pmp != null ? pmp.getDeals() : null; - final Deal topDeal = CollectionUtils.isNotEmpty(deals) ? deals.get(0) : null; - - impToTopDealMap.put(imp.getId(), topDeal != null ? topDeal.getId() : null); - } - - return !impToTopDealMap.containsValue(null) - ? new TopDealsReceivedTracker(impToTopDealMap) - : new NeverCompletedTracker(); - } - - private static class NeverCompletedTracker implements BidderRequestCompletionTracker { - - @Override - public Future future() { - return Future.failedFuture("No deals to wait for"); - } - - @Override - public void processBids(List bids) { - // no need to process bid when no deals to wait for - } - } - - private static class TopDealsReceivedTracker implements BidderRequestCompletionTracker { - - private final Map impToTopDealMap; - - private final Promise completionPromise; - - private TopDealsReceivedTracker(Map impToTopDealMap) { - this.impToTopDealMap = new HashMap<>(impToTopDealMap); - this.completionPromise = Promise.promise(); - } - - @Override - public Future future() { - return completionPromise.future(); - } - - @Override - public void processBids(List bids) { - if (completionPromise.future().isComplete()) { - return; - } - - bids.stream() - .map(BidderBid::getBid) - .filter(Objects::nonNull) - .map(this::toImpIdIfTopDeal) - .filter(Objects::nonNull) - .forEach(impToTopDealMap::remove); - - if (impToTopDealMap.isEmpty()) { - completionPromise.tryComplete(); - } - } - - private String toImpIdIfTopDeal(Bid bid) { - final String impId = bid.getImpid(); - final String dealId = bid.getDealid(); - if (StringUtils.isNoneBlank(impId, dealId)) { - final String topDealForImp = impToTopDealMap.get(impId); - if (topDealForImp != null && Objects.equals(dealId, topDealForImp)) { - return impId; - } - } - - return null; - } - } -} diff --git a/src/main/java/org/prebid/server/bidder/GenericBidder.java b/src/main/java/org/prebid/server/bidder/GenericBidder.java index a929e3673c2..a708de0ca1b 100644 --- a/src/main/java/org/prebid/server/bidder/GenericBidder.java +++ b/src/main/java/org/prebid/server/bidder/GenericBidder.java @@ -2,10 +2,8 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -14,14 +12,16 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; public class GenericBidder implements Bidder { @@ -35,15 +35,7 @@ public GenericBidder(String endpointUrl, JacksonMapper mapper) { @Override public final Result>> makeHttpRequests(BidRequest bidRequest) { - return Result.withValue( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(HttpUtil.headers()) - .body(mapper.encodeToBytes(bidRequest)) - .impIds(BidderUtil.impIds(bidRequest)) - .payload(bidRequest) - .build()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); } @Override @@ -64,29 +56,15 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + final Map impMap = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid, bidRequest.getImp()), bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, BidderUtil.getBidType(bid, impMap), bidResponse.getCur())) .toList(); } - private static BidType getBidType(Bid bid, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(bid.getImpid())) { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getXNative() != null) { - return BidType.xNative; - } else if (imp.getAudio() != null) { - return BidType.audio; - } - } - } - return BidType.banner; - } } diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 42976d52256..2a2fde46430 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -7,8 +7,6 @@ import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; @@ -28,12 +26,14 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -100,7 +100,8 @@ public Future requestBids(Bidder bidder, final List errors = httpRequestsWithErrors.getErrors(); final List> httpRequests = enrichRequests( bidderName, httpRequestsWithErrors.getValue(), requestHeaders, aliases, bidRequest); - recordBidderProvidedErrors(bidRejectionTracker, errors); + + rejectErrors(bidRejectionTracker, errors, BidRejectionReason.REQUEST_BLOCKED_GENERAL); if (CollectionUtils.isEmpty(httpRequests)) { return emptyBidderSeatBidWithErrors(errors); @@ -110,7 +111,7 @@ public Future requestBids(Bidder bidder, // stored response available only for single request interaction for the moment. final Stream>> httpCalls = isStoredResponse(httpRequests, storedResponse, bidderName) - ? Stream.of(makeStoredHttpCall(httpRequests.get(0), storedResponse)) + ? Stream.of(makeStoredHttpCall(httpRequests.getFirst(), storedResponse)) : httpRequests.stream().map(httpRequest -> doRequest(httpRequest, timeout)); // httpCalls contains recovered and mapped to succeeded Future with error inside @@ -144,11 +145,13 @@ private List> enrichRequests(String bidderName, .toList(); } - private static void recordBidderProvidedErrors(BidRejectionTracker rejectionTracker, List errors) { - errors.stream() + private static void rejectErrors(BidRejectionTracker bidRejectionTracker, + List bidderErrors, + BidRejectionReason reason) { + + bidderErrors.stream() .filter(error -> CollectionUtils.isNotEmpty(error.getImpIds())) - .forEach(error -> rejectionTracker.reject( - error.getImpIds(), BidRejectionReason.fromBidderError(error))); + .forEach(error -> bidRejectionTracker.reject(error.getImpIds(), reason)); } private boolean isStoredResponse(List> httpRequests, String storedResponse, String bidder) { @@ -159,7 +162,7 @@ private boolean isStoredResponse(List> httpRequests, String s if (httpRequests.size() > 1) { logger.warn(""" More than one request was created for stored response, when only single stored response \ - per bidder is supported for the moment. Request to real {0} bidder will be performed.""", + per bidder is supported for the moment. Request to real {} bidder will be performed.""", bidder); return false; } @@ -239,9 +242,9 @@ private static byte[] gzip(byte[] value) { * Produces {@link Future} with {@link BidderCall} containing request and error description. */ private static Future> failResponse(Throwable exception, HttpRequest httpRequest) { - logger.warn("Error occurred while sending HTTP request to a bidder url: {0} with message: {1}", + logger.warn("Error occurred while sending HTTP request to a bidder url: {} with message: {}", httpRequest.getUri(), exception.getMessage()); - logger.debug("Error occurred while sending HTTP request to a bidder url: {0}", + logger.debug("Error occurred while sending HTTP request to a bidder url: {}", exception, httpRequest.getUri()); final BidderError.Type errorType = @@ -374,16 +377,44 @@ private void handleBidderErrors(CompositeBidderResponse bidderResponse) { final List bidderErrors = bidderResponse != null ? bidderResponse.getErrors() : null; if (bidderErrors != null) { errorsRecorded.addAll(bidderErrors); - recordBidderProvidedErrors(bidRejectionTracker, bidderErrors); + rejectErrors(bidRejectionTracker, bidderErrors, BidRejectionReason.ERROR_GENERAL); } } private void handleBidderCallError(BidderCall bidderCall) { + final Set requestedImpIds = bidderCall.getRequest().getImpIds(); + if (CollectionUtils.isEmpty(requestedImpIds)) { + return; + } + + final Integer statusCode = Optional.ofNullable(bidderCall.getResponse()) + .map(HttpResponse::getStatusCode) + .orElse(null); + + if (statusCode != null && statusCode == HttpResponseStatus.SERVICE_UNAVAILABLE.code()) { + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + return; + } + + if (statusCode != null + && (statusCode < HttpResponseStatus.OK.code() + || statusCode >= HttpResponseStatus.BAD_REQUEST.code())) { + + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + return; + } + final BidderError callError = bidderCall.getError(); final BidderError.Type callErrorType = callError != null ? callError.getType() : null; - final Set requestedImpIds = bidderCall.getRequest().getImpIds(); - if (callErrorType != null && CollectionUtils.isNotEmpty(requestedImpIds)) { - bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.fromBidderError(callError)); + + if (callErrorType == null) { + return; + } + + if (callErrorType == BidderError.Type.timeout) { + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_TIMED_OUT); + } else { + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_GENERAL); } } diff --git a/src/main/java/org/prebid/server/bidder/aceex/AceexBidder.java b/src/main/java/org/prebid/server/bidder/aceex/AceexBidder.java index e5a616ab05d..841d888e064 100644 --- a/src/main/java/org/prebid/server/bidder/aceex/AceexBidder.java +++ b/src/main/java/org/prebid/server/bidder/aceex/AceexBidder.java @@ -45,7 +45,7 @@ public AceexBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final Imp firstImp = request.getImp().get(0); + final Imp firstImp = request.getImp().getFirst(); final ExtImpAceex extImpAceex; try { @@ -96,7 +96,7 @@ public final Result> makeBids(BidderCall httpCall, B private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { final List seatBids = ObjectUtil.getIfNotNull(bidResponse, BidResponse::getSeatbid); - final SeatBid firstSeatBid = CollectionUtils.isNotEmpty(seatBids) ? seatBids.get(0) : null; + final SeatBid firstSeatBid = CollectionUtils.isNotEmpty(seatBids) ? seatBids.getFirst() : null; if (firstSeatBid == null) { throw new PreBidException("Empty SeatBid array"); } diff --git a/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java b/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java index 4bd41d15c86..f44d3d6f934 100644 --- a/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java +++ b/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java @@ -53,7 +53,7 @@ public Result>> makeHttpRequests(BidRequest request final String url; try { - extImpAcuityads = parseImpExt(request.getImp().get(0)); + extImpAcuityads = parseImpExt(request.getImp().getFirst()); url = resolveEndpoint(extImpAcuityads.getHost(), extImpAcuityads.getAccountId()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -140,7 +140,7 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); final List bids = firstSeatBid.getBid(); if (CollectionUtils.isEmpty(bids)) { diff --git a/src/main/java/org/prebid/server/bidder/adelement/AdelementBidder.java b/src/main/java/org/prebid/server/bidder/adelement/AdelementBidder.java index ded4e7ef685..c61cb88be40 100644 --- a/src/main/java/org/prebid/server/bidder/adelement/AdelementBidder.java +++ b/src/main/java/org/prebid/server/bidder/adelement/AdelementBidder.java @@ -43,7 +43,7 @@ public AdelementBidder(String endpointUrl, JacksonMapper mapper) { @Override public final Result>> makeHttpRequests(BidRequest bidRequest) { - final Imp firstImp = bidRequest.getImp().get(0); + final Imp firstImp = bidRequest.getImp().getFirst(); final ExtImpAdelement extImpAdelement; try { @@ -71,7 +71,7 @@ private String resolveEndpoint(String supplyId) { public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + return Result.withValues(extractBids(bidResponse)); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse("Bad Server Response")); } catch (PreBidException e) { @@ -79,14 +79,14 @@ public Result> makeBids(BidderCall httpCall, BidRequ } } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private static List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); + return bidsFromResponse(bidResponse); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private static List bidsFromResponse(BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) @@ -103,7 +103,7 @@ private static BidType getBidType(Integer mType) { case 3 -> BidType.audio; case 4 -> BidType.xNative; - default -> throw new PreBidException("Unsupported mType " + mType); + case null, default -> throw new PreBidException("Unsupported mType " + mType); }; } } diff --git a/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java b/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java index d859ca2ae3d..22fc3dc5309 100644 --- a/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java +++ b/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java @@ -152,7 +152,7 @@ private static String getCurrency(BidRequest bidRequest) { final List currencies = bidRequest.getCur(); return CollectionUtils.isEmpty(currencies) ? DEFAULT_REQUEST_CURRENCY - : currencies.contains(DEFAULT_REQUEST_CURRENCY) ? DEFAULT_REQUEST_CURRENCY : currencies.get(0); + : currencies.contains(DEFAULT_REQUEST_CURRENCY) ? DEFAULT_REQUEST_CURRENCY : currencies.getFirst(); } private static HttpRequest createSingleRequest(String uri, Device device) { diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 7ad12b530aa..29ee5177d40 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -58,7 +58,7 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpAdhese extImpAdhese; try { - extImpAdhese = parseImpExt(request.getImp().get(0)); + extImpAdhese = parseImpExt(request.getImp().getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -89,7 +89,7 @@ private BidRequest modifyBidRequest(BidRequest bidRequest, ExtImpAdhese extImpAd final ObjectNode adheseExtInnerNode = mapper.mapper().valueToTree(parameterMap); final ObjectNode adheseExtNode = mapper.mapper().createObjectNode().set("adhese", adheseExtInnerNode); - final Imp imp = bidRequest.getImp().get(0).toBuilder() + final Imp imp = bidRequest.getImp().getFirst().toBuilder() .ext(adheseExtNode) .build(); @@ -166,7 +166,7 @@ private static BidType getBidType(BidRequest bidRequest) { throw new PreBidException("No Imps available"); } - final Imp firstImp = impList.get(0); + final Imp firstImp = impList.getFirst(); if (firstImp.getBanner() != null) { return BidType.banner; } else if (firstImp.getVideo() != null) { diff --git a/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java b/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java index d632fc37e21..44c3eaf45ab 100644 --- a/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java +++ b/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java @@ -246,11 +246,7 @@ private static BidderBid makeBidderBid(Bid bid, List imps, String currency) final Optional mfSuffix = getMfSuffix(bid.getImpid()); final Bid updatedBid = mfSuffix.map(suffix -> removeMfSuffixFromImpId(bid, suffix)).orElse(bid); - final BidType bidType = mfSuffix - .flatMap(AdkernelBidder::getTypeFromMsSuffix) - .orElseGet(() -> getTypeFromImp(updatedBid.getImpid(), imps)); - - return BidderBid.of(updatedBid, bidType, currency); + return BidderBid.of(updatedBid, getBidType(bid), currency); } private static Optional getMfSuffix(String impId) { @@ -265,35 +261,19 @@ private static Bid removeMfSuffixFromImpId(Bid bid, String mfSuffix) { .build(); } - private static Optional getTypeFromMsSuffix(String msSuffix) { - final BidType bidType = switch (msSuffix) { - case MF_SUFFIX_BANNER -> BidType.banner; - case MF_SUFFIX_VIDEO -> BidType.video; - case MF_SUFFIX_AUDIO -> BidType.audio; - case MF_SUFFIX_NATIVE -> BidType.xNative; - default -> null; - }; - - return Optional.ofNullable(bidType); - } - - private static BidType getTypeFromImp(String impId, List imps) { - for (Imp imp : imps) { - if (!imp.getId().equals(impId)) { - continue; - } - - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getAudio() != null) { - return BidType.audio; - } else if (imp.getXNative() != null) { - return BidType.xNative; - } + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); } - return BidType.video; + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unsupported MType %d".formatted(markupType)); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java b/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java index 5a762d81ec4..886a4a9ec48 100644 --- a/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java +++ b/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java @@ -40,7 +40,6 @@ public class AdkernelAdnBidder implements Bidder { private static final TypeReference> ADKERNELADN_EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String DEFAULT_DOMAIN = "tag.adkernel.com"; private static final String URL_PUBLISHER_ID_MACRO = "{{PublisherID}}"; private final String endpointUrl; @@ -141,7 +140,7 @@ private static void compatBannerImpression(Imp.ImpBuilder impBuilder, Banner com throw new PreBidException("Expected at least one banner.format entry or explicit w/h"); } - final Format format = compatBannerFormat.get(0); + final Format format = compatBannerFormat.getFirst(); final Banner.BannerBuilder bannerBuilder = compatBanner.toBuilder(); if (compatBannerFormat.size() > 1) { diff --git a/src/main/java/org/prebid/server/bidder/admatic/AdmaticBidder.java b/src/main/java/org/prebid/server/bidder/admatic/AdmaticBidder.java new file mode 100644 index 00000000000..9f564a7275a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/admatic/AdmaticBidder.java @@ -0,0 +1,125 @@ +package org.prebid.server.bidder.admatic; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.admatic.AdmaticImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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.Collectors; + +public class AdmaticBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String HOST_MACRO = "{{Host}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdmaticBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final AdmaticImpExt impExt = parseImpExt(imp); + final BidRequest modifiedBidRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + requests.add(BidderUtil.defaultRequest(modifiedBidRequest, resolveEndpoint(impExt), mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private AdmaticImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private String resolveEndpoint(AdmaticImpExt impExt) { + return endpointUrl.replace(HOST_MACRO, HttpUtil.encodeUrl(impExt.getHost())); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, + BidResponse bidResponse) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final Map impMap = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid, impMap), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid, Map impIdToImpMap) { + final String impId = bid.getImpid(); + return Optional.ofNullable(impIdToImpMap.get(impId)) + .map(imp -> { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + return null; + }) + .orElseThrow(() -> new PreBidException( + "The impression with ID %s is not present into the request".formatted(impId))); + } + +} diff --git a/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java b/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java index 19423e13ef7..4ddb522f74f 100644 --- a/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java +++ b/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java @@ -119,7 +119,7 @@ public Result> makeBids(BidderCall httpCall, BidRequ if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) - || CollectionUtils.isEmpty(bidResponse.getSeatbid().get(0).getBid())) { + || CollectionUtils.isEmpty(bidResponse.getSeatbid().getFirst().getBid())) { return Result.empty(); } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java index 3f6a30f38cf..672f31dd8af 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -2,19 +2,24 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; @@ -23,11 +28,12 @@ import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; -import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusResponse; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBidExt; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusNetBid; -import org.prebid.server.bidder.adnuntius.model.util.AdsUnitWithImpId; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -39,9 +45,12 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; @@ -56,8 +65,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.IntStream; -import java.util.stream.Stream; +import java.util.function.Function; +import java.util.stream.Collectors; public class AdnuntiusBidder implements Bidder { @@ -68,7 +77,6 @@ public class AdnuntiusBidder implements Bidder { private static final String TARGET_ID_DELIMITER = "-"; private static final String DEFAULT_PAGE = "unknown"; private static final String DEFAULT_NETWORK = "default"; - private static final String URL_NO_COOKIES_PARAMETER = "noCookies"; private static final BigDecimal PRICE_MULTIPLIER = BigDecimal.valueOf(1000); private final String endpointUrl; @@ -95,53 +103,16 @@ public Result>> makeHttpRequests(BidRequest r return Result.withError(BidderError.badInput(e.getMessage())); } - noCookies = resolveIsNoCookies(extImpAdnuntius); + noCookies = noCookies || resolveIsNoCookies(extImpAdnuntius); final String network = resolveNetwork(extImpAdnuntius); - networkToAdUnits.computeIfAbsent(network, n -> new ArrayList<>()) - .add(makeAdnuntiusAdUnit(imp, extImpAdnuntius)); + networkToAdUnits.computeIfAbsent(network, ignored -> new ArrayList<>()) + .add(makeAdUnit(imp, extImpAdnuntius)); } return Result.withValues(createHttpRequests(networkToAdUnits, request, noCookies)); } - private static AdnuntiusAdUnit makeAdnuntiusAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { - final String auId = extImpAdnuntius.getAuId(); - return AdnuntiusAdUnit.builder() - .auId(auId) - .targetId(auId + TARGET_ID_DELIMITER + imp.getId()) - .dimensions(createDimensions(imp)) - .maxDeals(resolveMaxDeals(extImpAdnuntius)) - .build(); - } - - private static List> createDimensions(Imp imp) { - final Banner banner = imp.getBanner(); - - if (CollectionUtils.isNotEmpty(banner.getFormat())) { - final List> formats = new ArrayList<>(); - for (Format format : banner.getFormat()) { - if (format.getW() != null && format.getH() != null) { - formats.add(List.of(format.getW(), format.getH())); - } - } - return formats; - } - - if (banner.getW() != null && banner.getH() != null) { - return Collections.singletonList(List.of(banner.getW(), banner.getH())); - } - - return null; - } - - private static Integer resolveMaxDeals(ExtImpAdnuntius extImpAdnuntius) { - if (extImpAdnuntius.getMaxDeals() != null && extImpAdnuntius.getMaxDeals() > 0) { - return extImpAdnuntius.getMaxDeals(); - } - return null; - } - private static void validateImp(Imp imp) { if (imp.getBanner() == null) { throw new PreBidException("Fail on Imp.Id=%s: Adnuntius supports only Banner".formatted(imp.getId())); @@ -159,59 +130,104 @@ private ExtImpAdnuntius parseImpExt(Imp imp) { private static boolean resolveIsNoCookies(ExtImpAdnuntius extImpAdnuntius) { return Optional.of(extImpAdnuntius) .map(ExtImpAdnuntius::getNoCookies) - .filter(BooleanUtils::isTrue) - .isPresent(); + .map(BooleanUtils::isTrue) + .orElse(false); } private static String resolveNetwork(ExtImpAdnuntius extImpAdnuntius) { return Optional.of(extImpAdnuntius) .map(ExtImpAdnuntius::getNetwork) - .filter(StringUtils::isNoneEmpty) + .filter(StringUtils::isNotEmpty) .orElse(DEFAULT_NETWORK); } - private List> createHttpRequests(Map> networkToAdUnits, - BidRequest request, Boolean noCookies) { + private static AdnuntiusAdUnit makeAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { + final String auId = extImpAdnuntius.getAuId(); + return AdnuntiusAdUnit.builder() + .auId(auId) + .targetId(targetId(auId, imp.getId())) + .dimensions(createDimensions(imp.getBanner())) + .maxDeals(resolveMaxDeals(extImpAdnuntius)) + .build(); + } - final List> adnuntiusRequests = new ArrayList<>(); + private static String targetId(String auId, String impId) { + return auId + TARGET_ID_DELIMITER + impId; + } - final AdnuntiusMetaData metaData = createMetaData(request.getUser()); - final String page = extractPage(request.getSite()); - final String uri = createUri(request, noCookies); - final Device device = request.getDevice(); + private static List> createDimensions(Banner banner) { + final List> formats = new ArrayList<>(); - for (List adUnits : networkToAdUnits.values()) { - final AdnuntiusRequest adnuntiusRequest = AdnuntiusRequest.of(adUnits, metaData, page); - adnuntiusRequests.add(createHttpRequest(adnuntiusRequest, uri, device)); + final List bannerFormat = ListUtils.emptyIfNull(banner.getFormat()); + for (Format format : bannerFormat) { + final Integer w = format.getW(); + final Integer h = format.getH(); + if (w != null && h != null) { + formats.add(List.of(w, h)); + } + } + if (!formats.isEmpty()) { + return formats; } - return adnuntiusRequests; + final Integer w = banner.getW(); + final Integer h = banner.getH(); + if (w != null && h != null) { + formats.add(List.of(w, h)); + } + + return formats.isEmpty() ? null : formats; } - private static AdnuntiusMetaData createMetaData(User user) { - final String userId = ObjectUtil.getIfNotNull(user, User::getId); - return StringUtils.isNotBlank(userId) ? AdnuntiusMetaData.of(userId) : null; + private static Integer resolveMaxDeals(ExtImpAdnuntius extImpAdnuntius) { + final Integer maxDeals = extImpAdnuntius.getMaxDeals(); + return maxDeals != null && maxDeals > 0 ? maxDeals : null; } - private static String extractPage(Site site) { - return StringUtils.defaultIfBlank(ObjectUtil.getIfNotNull(site, Site::getPage), DEFAULT_PAGE); + private List> createHttpRequests(Map> networkToAdUnits, + BidRequest request, + boolean noCookies) { + + final Site site = request.getSite(); + + final String uri = createUri(request, noCookies); + final String page = extractPage(site); + final ObjectNode data = extractData(site); + final AdnuntiusMetaData metaData = createMetaData(request.getUser()); + + final List> adnuntiusRequests = new ArrayList<>(); + + for (List adUnits : networkToAdUnits.values()) { + final AdnuntiusRequest adnuntiusRequest = AdnuntiusRequest.builder() + .adUnits(adUnits) + .context(page) + .keyValue(data) + .metaData(metaData) + .build(); + adnuntiusRequests.add(createHttpRequest(adnuntiusRequest, uri, request.getDevice())); + } + + return adnuntiusRequests; } private String createUri(BidRequest bidRequest, Boolean noCookies) { try { final URIBuilder uriBuilder = new URIBuilder(endpointUrl) - .addParameter("format", "json") + .addParameter("format", "prebid") .addParameter("tzo", getTimeZoneOffset()); final String gdpr = extractGdpr(bidRequest.getRegs()); - final String consent = extractConsent(bidRequest.getUser()); - if (StringUtils.isNoneEmpty(gdpr, consent)) { + if (StringUtils.isNotEmpty(gdpr)) { uriBuilder.addParameter("gdpr", gdpr); + } + + final String consent = extractConsent(bidRequest.getUser()); + if (StringUtils.isNotEmpty(consent)) { uriBuilder.addParameter("consentString", consent); } if (noCookies || extractNoCookies(bidRequest.getDevice())) { - uriBuilder.addParameter(URL_NO_COOKIES_PARAMETER, "true"); + uriBuilder.addParameter("noCookies", "true"); } return uriBuilder.build().toString(); @@ -225,36 +241,76 @@ private String getTimeZoneOffset() { } private static String extractGdpr(Regs regs) { - final Integer gdpr = ObjectUtil.getIfNotNull(ObjectUtil.getIfNotNull(regs, Regs::getExt), ExtRegs::getGdpr); - return gdpr != null ? gdpr.toString() : null; + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .map(Objects::toString) + .orElse(null); } private static String extractConsent(User user) { - return ObjectUtil.getIfNotNull(ObjectUtil.getIfNotNull(user, User::getExt), ExtUser::getConsent); + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(null); } - private static Boolean extractNoCookies(Device device) { + private static boolean extractNoCookies(Device device) { return Optional.ofNullable(device) .map(Device::getExt) .map(FlexibleExtension::getProperties) - .map(properties -> properties.get(URL_NO_COOKIES_PARAMETER)) + .map(properties -> properties.get("noCookies")) .filter(JsonNode::isBoolean) - .map(JsonNode::asBoolean) + .map(JsonNode::booleanValue) .orElse(false); } - private HttpRequest createHttpRequest(AdnuntiusRequest adnuntiusRequest, String uri, + private static String extractPage(Site site) { + return Optional.ofNullable(site) + .map(Site::getPage) + .filter(StringUtils::isNotEmpty) + .orElse(DEFAULT_PAGE); + } + + private static ObjectNode extractData(Site site) { + return Optional.ofNullable(site) + .map(Site::getExt) + .map(ExtSite::getData) + .orElse(null); + } + + private static AdnuntiusMetaData createMetaData(User user) { + final Optional userOptional = Optional.ofNullable(user); + return userOptional + .map(User::getId) + .filter(StringUtils::isNotEmpty) + .or(() -> userOptional + .map(User::getExt) + .map(ExtUser::getEids) + .filter(CollectionUtils::isNotEmpty) + .map(List::getFirst) + .map(Eid::getUids) + .filter(CollectionUtils::isNotEmpty) + .map(List::getFirst) + .map(Uid::getId)) + .map(AdnuntiusMetaData::of) + .orElse(null); + } + + private HttpRequest createHttpRequest(AdnuntiusRequest adnuntiusRequest, + String uri, Device device) { + return HttpRequest.builder() .method(HttpMethod.POST) - .headers(getHeaders(device)) + .headers(headers(device)) .uri(uri) .body(mapper.encodeToBytes(adnuntiusRequest)) .payload(adnuntiusRequest) .build(); } - private MultiMap getHeaders(Device device) { + private MultiMap headers(Device device) { final MultiMap headers = HttpUtil.headers(); if (device != null) { @@ -281,80 +337,90 @@ private List extractBids(BidRequest bidRequest, AdnuntiusResponse adn return Collections.emptyList(); } - final List adsUnits = adnuntiusResponse.getAdsUnits(); - final List imps = bidRequest.getImp(); - if (adsUnits.size() > imps.size()) { - throw new PreBidException("Impressions count is less then ads units count."); - } + final Map targetIdToAdsUnit = adnuntiusResponse.getAdsUnits().stream() + .filter(AdnuntiusBidder::validateAdsUnit) + .collect(Collectors.toMap( + AdnuntiusAdsUnit::getTargetId, + Function.identity(), + (first, second) -> second)); - final List validAdsUnitToImp = IntStream.range(0, adsUnits.size()) - .mapToObj(i -> AdsUnitWithImpId.of(adsUnits.get(i), imps.get(i), parseImpExt(imps.get(i)))) - .filter(adsUnitWithImpId -> validateAdsUnit(adsUnitWithImpId.getAdsUnit())) - .toList(); + String currency = null; + final List bids = new ArrayList<>(); - if (validAdsUnitToImp.isEmpty()) { - return Collections.emptyList(); - } + for (Imp imp : bidRequest.getImp()) { + final ExtImpAdnuntius extImpAdnuntius = parseImpExt(imp); + final String targetId = targetId(extImpAdnuntius.getAuId(), imp.getId()); - final String currency = extractCurrency(validAdsUnitToImp); - final Stream generalBids = validAdsUnitToImp.stream() - .map(adsUnitWithImpId -> makeGeneralBid(adsUnitWithImpId, currency)); + final AdnuntiusAdsUnit adsUnit = targetIdToAdsUnit.get(targetId); + if (adsUnit == null) { + continue; + } - final Stream dealBids = validAdsUnitToImp.stream() - .filter(adsUnitWithImpId -> CollectionUtils.isNotEmpty(adsUnitWithImpId.getAdsUnit().getDeals())) - .map(adsUnitWithImpId -> makeDealsBid(adsUnitWithImpId, currency)) - .filter(Objects::nonNull); + final AdnuntiusAd ad = adsUnit.getAds().getFirst(); + final String impId = imp.getId(); + final String bidType = extImpAdnuntius.getBidType(); + currency = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getCurrency); - return Stream.concat(generalBids, dealBids).toList(); - } - - private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { - final List ads = ObjectUtil.getIfNotNull(adsUnit, AdnuntiusAdsUnit::getAds); - return CollectionUtils.isNotEmpty(ads) && ads.get(0) != null; - } + bids.add(createBid(ad, bidRequest, adsUnit.getHtml(), impId, bidType)); - private static String extractCurrency(List adsUnits) { - final AdnuntiusBid bid = adsUnits.get(adsUnits.size() - 1).getAdsUnit().getAds().get(0).getBid(); - return ObjectUtil.getIfNotNull(bid, AdnuntiusBid::getCurrency); - } + for (AdnuntiusAd deal : ListUtils.emptyIfNull(adsUnit.getDeals())) { + bids.add(createBid(deal, bidRequest, deal.getHtml(), impId, bidType)); + } + } - private BidderBid makeGeneralBid(AdsUnitWithImpId adsUnitWithImpId, String currency) { - final AdnuntiusAdsUnit adsUnit = adsUnitWithImpId.getAdsUnit(); - final AdnuntiusAd ad = adsUnit.getAds().get(0); - final Bid bid = createBid(adsUnit, adsUnitWithImpId.getImp(), adsUnitWithImpId.getExtImpAdnuntius(), ad); - return BidderBid.of(bid, BidType.banner, currency); + final String lastCurrency = currency; + return bids.stream() + .map(bid -> BidderBid.of(bid, BidType.banner, lastCurrency)) + .toList(); } - private BidderBid makeDealsBid(AdsUnitWithImpId adsUnitWithImpId, String currency) { - final AdnuntiusAdsUnit adsUnit = adsUnitWithImpId.getAdsUnit(); - return adsUnit.getDeals().stream() - .map(adnuntiusAd -> - createBid(adsUnit, - adsUnitWithImpId.getImp(), - adsUnitWithImpId.getExtImpAdnuntius(), - adnuntiusAd)) - .map(bid -> BidderBid.of(bid, BidType.banner, currency)) - .findAny() - .orElse(null); + private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { + final List ads = adsUnit != null ? adsUnit.getAds() : null; + return CollectionUtils.isNotEmpty(ads) && ads.getFirst() != null; } - private static Bid createBid(AdnuntiusAdsUnit adsUnit, Imp imp, ExtImpAdnuntius extImpAdnuntius, AdnuntiusAd ad) { + private Bid createBid(AdnuntiusAd ad, BidRequest bidRequest, String adm, String impId, String bidType) { final String adId = ad.getAdId(); + final AdnuntiusBidExt bidExt = prepareBidExt(ad, bidRequest); + return Bid.builder() .id(adId) - .impid(imp.getId()) + .impid(impId) .w(parseMeasure(ad.getCreativeWidth())) .h(parseMeasure(ad.getCreativeHeight())) .adid(adId) + .dealid(ad.getDealId()) .cid(ad.getLineItemId()) .crid(ad.getCreativeId()) - .price(resolvePrice(ad, extImpAdnuntius.getBidType())) - .dealid(ad.getDealId()) - .adm(adsUnit.getHtml()) + .price(resolvePrice(ad, bidType)) + .adm(adm) .adomain(extractDomain(ad.getDestinationUrls())) + .ext(bidExt == null ? null : mapper.mapper().valueToTree(bidExt)) .build(); } + private static AdnuntiusBidExt prepareBidExt(AdnuntiusAd ad, BidRequest bidRequest) { + final ExtRegsDsa extRegsDsa = Optional.ofNullable(bidRequest.getRegs()) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .orElse(null); + + final AdnuntiusAdvertiser advertiser = ad.getAdvertiser(); + + if (advertiser != null && advertiser.getName() != null && extRegsDsa != null) { + final String legalName = ObjectUtils.firstNonNull(advertiser.getLegalName(), advertiser.getName()); + final ExtBidDsa dsa = ExtBidDsa.builder() + .adRender(0) + .paid(legalName) + .behalf(legalName) + .build(); + + return AdnuntiusBidExt.of(dsa); + } + + return null; + } + private static Integer parseMeasure(String measure) { try { return Integer.valueOf(measure); @@ -370,10 +436,10 @@ private static BigDecimal resolvePrice(AdnuntiusAd ad, String bidType) { amount = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getAmount); } if (StringUtils.endsWithIgnoreCase(bidType, "net")) { - amount = ObjectUtil.getIfNotNull(ad.getAdnuntiusNetBid(), AdnuntiusNetBid::getAmount); + amount = ObjectUtil.getIfNotNull(ad.getNetBid(), AdnuntiusNetBid::getAmount); } if (StringUtils.endsWithIgnoreCase(bidType, "gross")) { - amount = ObjectUtil.getIfNotNull(ad.getAdnuntiusGrossBid(), AdnuntiusGrossBid::getAmount); + amount = ObjectUtil.getIfNotNull(ad.getGrossBid(), AdnuntiusGrossBid::getAmount); } return amount != null ? amount.multiply(PRICE_MULTIPLIER) : BigDecimal.ZERO; @@ -384,7 +450,7 @@ private static List extractDomain(Map destinationUrls) { .filter(Objects::nonNull) .map(url -> url.split("/")) .filter(splintedUrl -> splintedUrl.length >= 2) - .map(splintedUrl -> splintedUrl[2].replaceAll("www\\.", "")) + .map(splintedUrl -> StringUtils.replace(splintedUrl[2], "www.", "")) .toList(); } } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java index d02557c690d..b077a3dce5f 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.adnuntius.model.request; -import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Value; @Value(staticConstructor = "of") public class AdnuntiusMetaData { - @JsonInclude(JsonInclude.Include.NON_EMPTY) String usi; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java index ed3fd313542..eafd24facbf 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java @@ -2,11 +2,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; import java.util.List; -@Value(staticConstructor = "of") +@Builder(toBuilder = true) +@Value public class AdnuntiusRequest { @JsonProperty("adUnits") @@ -18,4 +21,7 @@ public class AdnuntiusRequest { @JsonInclude(JsonInclude.Include.NON_EMPTY) String context; + + @JsonProperty("kv") + ObjectNode keyValue; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java index 59a61a06579..88367e172d6 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java @@ -13,10 +13,10 @@ public class AdnuntiusAd { AdnuntiusBid bid; @JsonProperty("netBid") - AdnuntiusNetBid adnuntiusNetBid; + AdnuntiusNetBid netBid; @JsonProperty("grossBid") - AdnuntiusGrossBid adnuntiusGrossBid; + AdnuntiusGrossBid grossBid; @JsonProperty("dealId") String dealId; @@ -36,6 +36,10 @@ public class AdnuntiusAd { @JsonProperty("lineItemId") String lineItemId; + String html; + @JsonProperty("destinationUrls") Map destinationUrls; + + AdnuntiusAdvertiser advertiser; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java new file mode 100644 index 00000000000..7c371a9b726 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AdnuntiusAdvertiser { + + @JsonProperty("legalName") + String legalName; + + String name; +} diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java new file mode 100644 index 00000000000..172a23471be --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; + +@Value(staticConstructor = "of") +public class AdnuntiusBidExt { + + ExtBidDsa dsa; +} diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/util/AdsUnitWithImpId.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/util/AdsUnitWithImpId.java deleted file mode 100644 index 339df114d43..00000000000 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/util/AdsUnitWithImpId.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.bidder.adnuntius.model.util; - -import com.iab.openrtb.request.Imp; -import lombok.Value; -import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; -import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; - -@Value(staticConstructor = "of") -public class AdsUnitWithImpId { - - AdnuntiusAdsUnit adsUnit; - - Imp imp; - - ExtImpAdnuntius extImpAdnuntius; -} diff --git a/src/main/java/org/prebid/server/bidder/adot/AdotBidder.java b/src/main/java/org/prebid/server/bidder/adot/AdotBidder.java index 54b8cf8cf12..49bbb0456b8 100644 --- a/src/main/java/org/prebid/server/bidder/adot/AdotBidder.java +++ b/src/main/java/org/prebid/server/bidder/adot/AdotBidder.java @@ -39,7 +39,7 @@ public class AdotBidder implements Bidder { private static final List ALLOWED_BID_TYPES = Arrays.asList(BidType.banner, BidType.video, BidType.xNative); private static final String PRICE_MACRO = "${AUCTION_PRICE}"; - private static final String PUBLISHER_MACRO = "{PUBLISHER_PATH}"; + private static final String PUBLISHER_MACRO = "{{PUBLISHER_PATH}}"; private static final TypeReference> ADOT_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -56,7 +56,7 @@ public AdotBidder(String endpointUrl, JacksonMapper mapper) { public Result>> makeHttpRequests(BidRequest bidRequest) { final List errors = new ArrayList<>(); - final Imp firstImp = bidRequest.getImp().get(0); + final Imp firstImp = bidRequest.getImp().getFirst(); final String publisherPath = StringUtils.defaultString( ObjectUtil.getIfNotNull(parseImpExt(firstImp), ExtImpAdot::getPublisherPath)); diff --git a/src/main/java/org/prebid/server/bidder/adpone/AdponeBidder.java b/src/main/java/org/prebid/server/bidder/adpone/AdponeBidder.java index 21bf160eab6..42e5534a6e8 100644 --- a/src/main/java/org/prebid/server/bidder/adpone/AdponeBidder.java +++ b/src/main/java/org/prebid/server/bidder/adpone/AdponeBidder.java @@ -37,7 +37,7 @@ public AdponeBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { try { - mapper.mapper().convertValue(bidRequest.getImp().get(0).getExt().get("bidder"), ExtImpAdpone.class); + mapper.mapper().convertValue(bidRequest.getImp().getFirst().getExt().get("bidder"), ExtImpAdpone.class); } catch (IllegalArgumentException e) { return Result.withError(BidderError.badInput(e.getMessage())); } diff --git a/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java b/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java index 2cb736f2aa3..f78d6c34f49 100644 --- a/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java +++ b/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java @@ -122,36 +122,34 @@ public final Result> makeBids(BidderCall httpCall, B try { final List errors = new ArrayList<>(); final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + return Result.of(extractBids(bidResponse, errors), errors); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse, - List errors) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse, errors); + return bidsFromResponse(bidResponse, errors); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse, - List errors) { + private static List bidsFromResponse(BidResponse bidResponse, List errors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> resolveBidderBid(bid, bidResponse.getCur(), bidRequest.getImp(), errors)) + .map(bid -> resolveBidderBid(bid, bidResponse.getCur(), errors)) .filter(Objects::nonNull) .toList(); } - private static BidderBid resolveBidderBid(Bid bid, String currency, List imps, List errors) { + private static BidderBid resolveBidderBid(Bid bid, String currency, List errors) { final BidType bidType; try { - bidType = getBidType(bid.getImpid(), imps); + bidType = getBidType(bid); } catch (PreBidException e) { errors.add(BidderError.badServerResponse(e.getMessage())); return null; @@ -159,22 +157,19 @@ private static BidderBid resolveBidderBid(Bid bid, String currency, List im return BidderBid.of(bid, bidType, currency); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getBanner() != null) { - return BidType.banner; - } - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - throw new PreBidException("Unknown impression type for ID: '%s'".formatted(impId)); - } + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); } - throw new PreBidException("Failed to find impression for ID: '%s'".formatted(impId)); + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid()); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/adrino/AdrinoBidder.java b/src/main/java/org/prebid/server/bidder/adrino/AdrinoBidder.java deleted file mode 100644 index 2e79b69f0aa..00000000000 --- a/src/main/java/org/prebid/server/bidder/adrino/AdrinoBidder.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.prebid.server.bidder.adrino; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class AdrinoBidder implements Bidder { - - private final String endpointUrl; - private final JacksonMapper mapper; - - public AdrinoBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public final Result>> makeHttpRequests(BidRequest request) { - return Result.withValue(BidderUtil.defaultRequest(request, endpointUrl, mapper)); - } - - @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse)); - } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private static List extractBids(BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - return bidsFromResponse(bidResponse); - } - - private static List bidsFromResponse(BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.xNative, bidResponse.getCur())) - .toList(); - } -} diff --git a/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java b/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java index b5d23fc60fe..76e716325f4 100644 --- a/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java +++ b/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java @@ -11,6 +11,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adtarget.proto.AdtargetImpExt; +import org.prebid.server.bidder.adtarget.proto.ExtImpAdtargetBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -67,16 +68,16 @@ private Result>> mapSourceIdToImp(List imps) { final Map> sourceToImps = new HashMap<>(); for (Imp imp : imps) { final ExtImpAdtarget extImpAdtarget; + final Integer sourceId; try { validateImpression(imp); extImpAdtarget = parseImpAdtarget(imp); + sourceId = resolveSourceId(imp.getId(), extImpAdtarget.getSourceId()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); continue; } - final Imp updatedImp = updateImp(imp, extImpAdtarget); - - final Integer sourceId = extImpAdtarget.getSourceId(); + final Imp updatedImp = updateImp(imp, sourceId, extImpAdtarget); sourceToImps.computeIfAbsent(sourceId, ignored -> new ArrayList<>()).add(updatedImp); } return Result.of(sourceToImps, errors); @@ -98,13 +99,14 @@ private static void validateImpression(Imp imp) { } final ObjectNode impExt = imp.getExt(); - if (impExt == null || impExt.size() == 0) { + if (impExt == null || impExt.isEmpty()) { throw new PreBidException("ignoring imp id=%s, extImpBidder is empty".formatted(impId)); } } - private Imp updateImp(Imp imp, ExtImpAdtarget extImpAdtarget) { - final AdtargetImpExt adtargetImpExt = AdtargetImpExt.of(extImpAdtarget); + private Imp updateImp(Imp imp, Integer sourceId, ExtImpAdtarget extImpAdtarget) { + final AdtargetImpExt adtargetImpExt = AdtargetImpExt.of( + ExtImpAdtargetBidRequest.from(sourceId, extImpAdtarget)); final BigDecimal bidFloor = extImpAdtarget.getBidFloor(); return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) @@ -112,6 +114,14 @@ private Imp updateImp(Imp imp, ExtImpAdtarget extImpAdtarget) { .build(); } + private static Integer resolveSourceId(String impId, String sourceId) { + try { + return sourceId == null ? 0 : Integer.parseInt(sourceId); + } catch (NumberFormatException e) { + throw new PreBidException("ignoring imp id=%s, aid parsing err: %s".formatted(impId, e.getMessage())); + } + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { final List errors = new ArrayList<>(); diff --git a/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java b/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java index 4fc2177fbf0..3a2ec0278f4 100644 --- a/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java +++ b/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java @@ -1,14 +1,11 @@ package org.prebid.server.bidder.adtarget.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.proto.openrtb.ext.request.adtarget.ExtImpAdtarget; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdtargetImpExt { @JsonProperty("adtarget") - ExtImpAdtarget extImpAdtarget; + ExtImpAdtargetBidRequest extImp; } diff --git a/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java b/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java new file mode 100644 index 00000000000..0abfc880e74 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java @@ -0,0 +1,31 @@ +package org.prebid.server.bidder.adtarget.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.adtarget.ExtImpAdtarget; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpAdtargetBidRequest { + + @JsonProperty("aid") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; + + public static ExtImpAdtargetBidRequest from(Integer sourceId, ExtImpAdtarget impExt) { + return ExtImpAdtargetBidRequest.of( + sourceId, + impExt.getPlacementId(), + impExt.getSiteId(), + impExt.getBidFloor()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java b/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java index 95ec877d975..404d83b9052 100644 --- a/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java +++ b/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java @@ -13,6 +13,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adtelligent.proto.AdtelligentImpExt; +import org.prebid.server.bidder.adtelligent.proto.ExtImpAdtelligentBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -86,16 +87,17 @@ private Result>> mapSourceIdToImp(List imps) { final Map> sourceToImps = new HashMap<>(); for (final Imp imp : imps) { final ExtImpAdtelligent extImpAdtelligent; + final Integer sourceId; try { validateImpression(imp); extImpAdtelligent = getExtImpAdtelligent(imp); + sourceId = resolveSourceId(imp.getId(), extImpAdtelligent.getSourceId()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); continue; } - final Imp updatedImp = updateImp(imp, extImpAdtelligent); + final Imp updatedImp = updateImp(imp, sourceId, extImpAdtelligent); - final Integer sourceId = extImpAdtelligent.getSourceId(); final List sourceIdImps = sourceToImps.get(sourceId); if (sourceIdImps == null) { sourceToImps.put(sourceId, new ArrayList<>(Collections.singleton(updatedImp))); @@ -156,7 +158,7 @@ private void validateImpression(Imp imp) { } final ObjectNode impExt = imp.getExt(); - if (impExt == null || impExt.size() == 0) { + if (impExt == null || impExt.isEmpty()) { throw new PreBidException("ignoring imp id=%s, extImpBidder is empty".formatted(impId)); } } @@ -164,8 +166,9 @@ private void validateImpression(Imp imp) { /** * Updates {@link Imp} with bidfloor if it is present in imp.ext.bidder */ - private Imp updateImp(Imp imp, ExtImpAdtelligent extImpAdtelligent) { - final AdtelligentImpExt adtelligentImpExt = AdtelligentImpExt.of(extImpAdtelligent); + private Imp updateImp(Imp imp, Integer sourceId, ExtImpAdtelligent extImpAdtelligent) { + final AdtelligentImpExt adtelligentImpExt = AdtelligentImpExt.of( + ExtImpAdtelligentBidRequest.from(sourceId, extImpAdtelligent)); final BigDecimal bidFloor = extImpAdtelligent.getBidFloor(); return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) @@ -173,6 +176,14 @@ private Imp updateImp(Imp imp, ExtImpAdtelligent extImpAdtelligent) { .build(); } + private static Integer resolveSourceId(String impId, String sourceId) { + try { + return sourceId == null ? 0 : Integer.parseInt(sourceId); + } catch (NumberFormatException e) { + throw new PreBidException("ignoring imp id=%s, aid parsing err: %s".formatted(impId, e.getMessage())); + } + } + /** * Extracts {@link Bid}s from response. */ diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java b/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java index ac23f7dd218..fc0bdd642a7 100644 --- a/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java +++ b/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java @@ -1,14 +1,11 @@ package org.prebid.server.bidder.adtelligent.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.proto.openrtb.ext.request.adtelligent.ExtImpAdtelligent; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdtelligentImpExt { @JsonProperty("adtelligent") - ExtImpAdtelligent extImpAdtelligent; + ExtImpAdtelligentBidRequest extImp; } diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java b/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java new file mode 100644 index 00000000000..e543e6a8e39 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java @@ -0,0 +1,31 @@ +package org.prebid.server.bidder.adtelligent.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.adtelligent.ExtImpAdtelligent; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpAdtelligentBidRequest { + + @JsonProperty("aid") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; + + public static ExtImpAdtelligentBidRequest from(Integer sourceId, ExtImpAdtelligent impExt) { + return ExtImpAdtelligentBidRequest.of( + sourceId, + impExt.getPlacementId(), + impExt.getSiteId(), + impExt.getBidFloor()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java new file mode 100644 index 00000000000..f780a020732 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java @@ -0,0 +1,145 @@ +package org.prebid.server.bidder.adtonos; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adtonos.ExtImpAdtonos; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class AdtonosBidder implements Bidder { + + private static final TypeReference> ADTONOS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{PublisherId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdtonosBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpAdtonos impExt = parseImpExt(bidRequest.getImp().getFirst()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(impExt), mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpAdtonos parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ADTONOS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext.bidder for impression index 0. Error Infomation: " + e.getMessage()); + } + } + + private String makeUrl(ExtImpAdtonos extImp) { + return endpointUrl.replace(PUBLISHER_ID_MACRO, extImp.getSupplierId()); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + final List bids = extractBids(bidResponse, httpCall.getRequest().getPayload(), errors); + + return Result.of(bids, errors); + } + + private static List extractBids(BidResponse bidResponse, + BidRequest bidRequest, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), bidRequest, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, BidRequest bidRequest, List errors) { + try { + return BidderBid.of(bid, resolveBidType(bid, bidRequest.getImp()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType resolveBidType(Bid bid, List imps) throws PreBidException { + final Integer markupType = bid.getMtype(); + if (markupType != null) { + switch (markupType) { + case 1 -> { + return BidType.banner; + } + case 2 -> { + return BidType.video; + } + case 3 -> { + return BidType.audio; + } + case 4 -> { + return BidType.xNative; + } + } + } + + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getVideo() != null) { + return BidType.video; + } + throw new PreBidException("Unsupported bidtype for bid: " + bid.getId()); + } + } + + throw new PreBidException("Failed to find impression: " + impId); + } +} diff --git a/src/main/java/org/prebid/server/bidder/advangelists/AdvangelistsBidder.java b/src/main/java/org/prebid/server/bidder/advangelists/AdvangelistsBidder.java index 7c071ac370e..0075573bbc8 100644 --- a/src/main/java/org/prebid/server/bidder/advangelists/AdvangelistsBidder.java +++ b/src/main/java/org/prebid/server/bidder/advangelists/AdvangelistsBidder.java @@ -127,7 +127,7 @@ private static Banner modifyImpBanner(Banner banner) { } final List formatSkipFirst = bannerFormats.subList(1, bannerFormats.size()); - final Format firstFormat = bannerFormats.get(0); + final Format firstFormat = bannerFormats.getFirst(); return banner.toBuilder() .format(formatSkipFirst) @@ -211,7 +211,7 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); final List bids = firstSeatBid.getBid(); if (CollectionUtils.isEmpty(bids)) { diff --git a/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java b/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java index cc62739b8f0..8c1de16d03d 100644 --- a/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java +++ b/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; @@ -55,20 +56,23 @@ public AdviewBidder(String endpointUrl, @Override public Result>> makeHttpRequests(BidRequest request) { - final Imp firstImp = request.getImp().get(0); - final ExtImpAdview extImpAdview; - final BidRequest modifiedBidRequest; - - try { - extImpAdview = parseExtImp(firstImp); - final Price bidFloorPrice = resolveBidFloor(firstImp, request); - modifiedBidRequest = modifyRequest(request, extImpAdview.getMasterTagId(), bidFloorPrice); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp: request.getImp()) { + try { + final ExtImpAdview extImp = parseExtImp(imp); + final Price bidFloorPrice = resolveBidFloor(imp, request); + final Imp modifiedImp = modifyImp(imp, extImp.getMasterTagId(), bidFloorPrice); + final BidRequest modifiedRequest = modifyRequest(request, modifiedImp); + final String resolvedUrl = resolveEndpoint(extImp.getAccountId()); + httpRequests.add(BidderUtil.defaultRequest(modifiedRequest, resolvedUrl, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } } - return Result.withValue( - BidderUtil.defaultRequest(modifiedBidRequest, resolveEndpoint(extImpAdview.getAccountId()), mapper)); + return Result.of(httpRequests, errors); } private ExtImpAdview parseExtImp(Imp imp) { @@ -99,19 +103,13 @@ private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidR } } - private static BidRequest modifyRequest(BidRequest bidRequest, String masterTagId, Price bidFloorPrice) { + private static BidRequest modifyRequest(BidRequest bidRequest, Imp modifiedImp) { return bidRequest.toBuilder() - .imp(modifyImps(bidRequest.getImp(), masterTagId, bidFloorPrice)) + .imp(Collections.singletonList(modifiedImp)) .cur(Collections.singletonList(BIDDER_CURRENCY)) .build(); } - private static List modifyImps(List imps, String masterTagId, Price bidFloorPrice) { - final List modifiedImps = new ArrayList<>(imps); - modifiedImps.set(0, modifyImp(imps.get(0), masterTagId, bidFloorPrice)); - return modifiedImps; - } - private static Imp modifyImp(Imp imp, String masterTagId, Price bidFloorPrice) { return imp.toBuilder() .tagid(masterTagId) @@ -124,7 +122,7 @@ private static Imp modifyImp(Imp imp, String masterTagId, Price bidFloorPrice) { private static Banner resolveBanner(Banner banner) { final List formats = banner != null ? banner.getFormat() : null; if (CollectionUtils.isNotEmpty(formats)) { - final Format firstFormat = formats.get(0); + final Format firstFormat = formats.getFirst(); return firstFormat != null ? banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build() : banner; @@ -140,40 +138,54 @@ private String resolveEndpoint(String accountId) { public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); - } catch (DecodeException e) { + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); + return bidsFromResponse(bidResponse, errors); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private static List bidsFromResponse(BidResponse bidResponse, List errors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidMediaType(bid.getImpid(), bidRequest.getImp()), - bidResponse.getCur())) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) .toList(); } - private static BidType getBidMediaType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getXNative() != null) { - return BidType.xNative; - } - } + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + final BidType mediaType = getBidMediaType(bid); + return BidderBid.of(bid, mediaType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; } - return BidType.banner; + + } + + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid()); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidder.java b/src/main/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidder.java index 4c13f48efcf..4c0b4510f80 100644 --- a/src/main/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidder.java +++ b/src/main/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidder.java @@ -70,7 +70,7 @@ public Result>> makeHttpRequests(BidRequest request } } - if (errors.size() > 0) { + if (!errors.isEmpty()) { return Result.withErrors(errors); } diff --git a/src/main/java/org/prebid/server/bidder/aidem/AidemBidder.java b/src/main/java/org/prebid/server/bidder/aidem/AidemBidder.java index f78e7ebeffc..9c4d362cbde 100644 --- a/src/main/java/org/prebid/server/bidder/aidem/AidemBidder.java +++ b/src/main/java/org/prebid/server/bidder/aidem/AidemBidder.java @@ -46,7 +46,7 @@ public AidemBidder(String endpointUrl, JacksonMapper mapper) { @Override public final Result>> makeHttpRequests(BidRequest bidRequest) { try { - final ExtImpAidem impExt = parseImpExt(bidRequest.getImp().get(0)); + final ExtImpAidem impExt = parseImpExt(bidRequest.getImp().getFirst()); return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(impExt), mapper)); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -75,13 +75,12 @@ public Result> makeBids(BidderCall httpCall, BidRequ } final List errors = new ArrayList<>(); - final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); + final List bids = extractBids(bidResponse, errors); return Result.of(bids, errors); } - private static List extractBids(BidRequest bidRequest, - BidResponse bidResponse, + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); @@ -93,12 +92,12 @@ private static List extractBids(BidRequest bidRequest, .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> makeBidderBid(bid, bidRequest.getImp(), bidResponse.getCur(), errors)) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) .filter(Objects::nonNull) .toList(); } - private static BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { try { return BidderBid.of(bid, resolveBidType(bid), currency); } catch (PreBidException e) { diff --git a/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java b/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java index 43549c101e8..b2b02f4b3bb 100644 --- a/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java +++ b/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java @@ -46,9 +46,9 @@ public class AlgorixBidder implements Bidder { new TypeReference<>() { }; - private static final String URL_REGION_MACRO = "{HOST}"; - private static final String URL_SID_MACRO = "{SID}"; - private static final String URL_TOKEN_MACRO = "{TOKEN}"; + private static final String URL_REGION_MACRO = "{{HOST}}"; + private static final String URL_SID_MACRO = "{{SID}}"; + private static final String URL_TOKEN_MACRO = "{{TOKEN}}"; private static final int FIRST_INDEX = 0; @@ -115,7 +115,7 @@ private Imp updateBannerImp(Imp imp) { final Banner banner = imp.getBanner(); if (!(isValidSizeValue(banner.getW()) && isValidSizeValue(banner.getH())) && CollectionUtils.isNotEmpty(banner.getFormat())) { - final Format firstFormat = banner.getFormat().get(FIRST_INDEX); + final Format firstFormat = banner.getFormat().getFirst(); return imp.toBuilder() .banner(banner.toBuilder() .w(firstFormat.getW()) diff --git a/src/main/java/org/prebid/server/bidder/alkimi/AlkimiBidder.java b/src/main/java/org/prebid/server/bidder/alkimi/AlkimiBidder.java index 9d729408634..03a05d1b1aa 100644 --- a/src/main/java/org/prebid/server/bidder/alkimi/AlkimiBidder.java +++ b/src/main/java/org/prebid/server/bidder/alkimi/AlkimiBidder.java @@ -36,8 +36,6 @@ public class AlkimiBidder implements Bidder { private final String endpointUrl; private final JacksonMapper mapper; - private static final String TYPE_BANNER = "Banner"; - private static final String TYPE_VIDEO = "Video"; private static final String PRICE_MACRO = "${AUCTION_PRICE}"; private static final TypeReference> ALKIMI_EXT_TYPE_REFERENCE = new TypeReference<>() { diff --git a/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java b/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java index 89ae201965c..bed5622ec06 100644 --- a/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java +++ b/src/main/java/org/prebid/server/bidder/amx/AmxBidder.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.amx; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; @@ -10,6 +11,7 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; @@ -25,6 +27,8 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.amx.ExtImpAmx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -90,7 +94,7 @@ public Result>> makeHttpRequests(BidRequest request final BidRequest outgoingRequest = createOutgoingRequest(request, publisherId, modifiedImps); return Result.of(Collections.singletonList( - BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)), + BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)), errors); } @@ -169,13 +173,13 @@ private BidderBid createBidderBid(Bid bid, String cur, List errors) errors.add(BidderError.badInput(e.getMessage())); return null; } - - return BidderBid.of(bid, getBidType(amxBidExt), cur); + // TODO: After adding support to change seat data, add bid.ext bidderCode processing + return BidderBid.of(resolveBid(bid, amxBidExt.getDemandSource()), getBidType(amxBidExt), cur); } private AmxBidExt parseBidderExt(ObjectNode ext) { - if (ext == null || StringUtils.isBlank(ext.toPrettyString())) { - return AmxBidExt.of(null, null); + if (ext == null || ext.isEmpty()) { + return AmxBidExt.empty(); } try { @@ -194,5 +198,36 @@ private BidType getBidType(AmxBidExt amxBidExt) { return BidType.banner; } } + + private Bid resolveBid(Bid bid, String demandSource) { + final List aDomains = bid.getAdomain(); + if (CollectionUtils.isEmpty(aDomains) && StringUtils.isBlank(demandSource)) { + return bid; + } + + return bid.toBuilder().ext(resolveBidExt(demandSource, aDomains, bid.getExt())).build(); + } + + private ObjectNode resolveBidExt(String demandSource, List aDomains, ObjectNode bidExt) { + final ObjectNode bidExtUpdated = bidExt != null && !bidExt.isMissingNode() + ? bidExt + : mapper.mapper().createObjectNode(); + final JsonNode bidExtPrebid = resolveBidExtPrebid(demandSource, aDomains, bidExtUpdated.get("prebid")); + + return bidExtUpdated.set("prebid", bidExtPrebid); + } + + private ObjectNode resolveBidExtPrebid(String demandSource, List aDomains, JsonNode bidExtPrebid) { + final ExtBidPrebidMeta extBidPrebidMeta = ExtBidPrebidMeta.builder() + .demandSource(demandSource) + .advertiserDomains(aDomains) + .build(); + if (bidExtPrebid == null || bidExtPrebid.isMissingNode()) { + return mapper.mapper().valueToTree(ExtBidPrebid.builder().meta(extBidPrebidMeta).build()); + } + + final ObjectNode bidExtPrebidCasted = (ObjectNode) bidExtPrebid; + return bidExtPrebidCasted.set("meta", mapper.mapper().valueToTree(extBidPrebidMeta)); + } } diff --git a/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java b/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java index d0f9da885b8..2d9da9a78d2 100644 --- a/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java +++ b/src/main/java/org/prebid/server/bidder/amx/model/AmxBidExt.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.amx.model; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @@ -8,10 +7,18 @@ public class AmxBidExt { @JsonProperty("ct") - @JsonInclude(JsonInclude.Include.NON_EMPTY) Integer creativeType; @JsonProperty("startdelay") - @JsonInclude(JsonInclude.Include.NON_EMPTY) Integer startDelay; + + @JsonProperty("ds") + String demandSource; + + @JsonProperty("bc") + String bidderCode; + + public static AmxBidExt empty() { + return AmxBidExt.of(null, null, null, null); + } } diff --git a/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java b/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java index 584a72cf67f..e559d0c8412 100644 --- a/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java +++ b/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java @@ -30,6 +30,7 @@ import org.prebid.server.bidder.appnexus.proto.AppnexusImpExtAppnexus; import org.prebid.server.bidder.appnexus.proto.AppnexusKeyVal; import org.prebid.server.bidder.appnexus.proto.AppnexusReqExtAppnexus; +import org.prebid.server.bidder.appnexus.proto.AppnexusExtImp; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -39,7 +40,6 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.UpdateResult; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -54,7 +54,7 @@ import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; -import javax.validation.ValidationException; +import jakarta.validation.ValidationException; import java.math.BigDecimal; import java.net.URISyntaxException; import java.util.ArrayList; @@ -76,9 +76,6 @@ public class AppnexusBidder implements Bidder { private static final String POD_SEPARATOR = "_"; private static final int MAX_IMP_PER_REQUEST = 10; - private static final TypeReference> APPNEXUS_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; private static final TypeReference>> KEYWORDS_OBJECT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -112,10 +109,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { - final ExtImpAppnexus extImpAppnexus = parseImpExt(imp); + final AppnexusExtImp extImp = parseImpExt(imp); + final ExtImpAppnexus extImpAppnexus = extImp.getBidder(); + final String gpid = extImp.getGpid(); validateExtImpAppnexus(extImpAppnexus, memberValidator, generateAdPodIdValidator); - updatedImps.add(updateImp(imp, extImpAppnexus, defaultDisplayManagerVer)); + updatedImps.add(updateImp(imp, extImpAppnexus, gpid, defaultDisplayManagerVer)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } catch (ValidationException e) { @@ -162,9 +161,9 @@ private String defaultDisplayManagerVer(BidRequest bidRequest) { : null; } - private ExtImpAppnexus parseImpExt(Imp imp) { + private AppnexusExtImp parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), APPNEXUS_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), AppnexusExtImp.class); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage(), e); } @@ -190,7 +189,7 @@ private static void validateExtImpAppnexus(ExtImpAppnexus extImpAppnexus, } } - private Imp updateImp(Imp imp, ExtImpAppnexus extImpAppnexus, String defaultDisplayManagerVer) { + private Imp updateImp(Imp imp, ExtImpAppnexus extImpAppnexus, String gpid, String defaultDisplayManagerVer) { final String invCode = extImpAppnexus.getInvCode(); final BigDecimal impBidFloor = imp.getBidfloor(); final BigDecimal extBidFloor = extImpAppnexus.getReserve(); @@ -205,7 +204,7 @@ private Imp updateImp(Imp imp, ExtImpAppnexus extImpAppnexus, String defaultDisp .displaymanagerver(StringUtils.isBlank(displayManagerVer) && defaultDisplayManagerVer != null ? defaultDisplayManagerVer : displayManagerVer) - .ext(makeImpExt(extImpAppnexus)) + .ext(makeImpExt(extImpAppnexus, gpid)) .build(); } @@ -218,7 +217,7 @@ private static Banner updateBanner(Banner banner, ExtImpAppnexus extImpAppnexus) final Integer height = banner.getH(); final List formats = banner.getFormat(); final Format firstFormat = CollectionUtils.isNotEmpty(formats) - ? formats.get(0) + ? formats.getFirst() : null; final boolean replaceWithFirstFormat = firstFormat != null && width == null && height == null; @@ -245,7 +244,7 @@ private static Integer resolvePosition(String position) { }; } - private ObjectNode makeImpExt(ExtImpAppnexus extImpAppnexus) { + private ObjectNode makeImpExt(ExtImpAppnexus extImpAppnexus, String gpid) { final AppnexusImpExtAppnexus ext = AppnexusImpExtAppnexus.builder() .placementId(extImpAppnexus.getPlacementId()) .trafficSourceCode(extImpAppnexus.getTrafficSourceCode()) @@ -256,7 +255,7 @@ private ObjectNode makeImpExt(ExtImpAppnexus extImpAppnexus) { .externalImpId(extImpAppnexus.getExternalImpId()) .build(); - return mapper.mapper().valueToTree(AppnexusImpExt.of(ext)); + return mapper.mapper().valueToTree(AppnexusImpExt.of(ext, gpid)); } private String readKeywords(JsonNode keywords) { diff --git a/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusExtImp.java b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusExtImp.java new file mode 100644 index 00000000000..c3164606fb3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusExtImp.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.appnexus.proto; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.appnexus.ExtImpAppnexus; + +@Value(staticConstructor = "of") +public class AppnexusExtImp { + + ExtImpAppnexus bidder; + + String gpid; + +} diff --git a/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusImpExt.java b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusImpExt.java index 76322407c82..dac8c6306c3 100644 --- a/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusImpExt.java +++ b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusImpExt.java @@ -6,4 +6,7 @@ public class AppnexusImpExt { AppnexusImpExtAppnexus appnexus; + + String gpid; + } diff --git a/src/main/java/org/prebid/server/bidder/aso/AsoBidder.java b/src/main/java/org/prebid/server/bidder/aso/AsoBidder.java new file mode 100644 index 00000000000..62307fb7bf8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/aso/AsoBidder.java @@ -0,0 +1,162 @@ +package org.prebid.server.bidder.aso; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.aso.ExtImpAso; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class AsoBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String ZONE_MACRO = "{{ZoneID}}"; + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AsoBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpAso extImp = parseImpExt(imp); + final BidRequest modifiedRequest = modifyBidRequest(request, imp); + httpRequests.add(makeHttpRequest(modifiedRequest, extImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpAso parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private BidRequest modifyBidRequest(BidRequest request, Imp imp) { + return request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, ExtImpAso extImp) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImp.getZone())) + .impIds(BidderUtil.impIds(request)) + .headers(HttpUtil.headers()) + .payload(request) + .body(mapper.encodeToBytes(request)) + .build(); + } + + private String makeUrl(Integer zone) { + return endpointUrl.replace(ZONE_MACRO, zone.toString()); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || bidResponse.getSeatbid() == null) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType mediaType = getMediaType(bid, errors); + + if (mediaType == null) { + return null; + } + + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + final Bid modifiedBid = bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .build(); + + return BidderBid.of(modifiedBid, mediaType, currency); + } + + private BidType getMediaType(Bid bid, List errors) { + try { + return Optional.ofNullable(bid.getExt()) + .map(ext -> mapper.mapper().convertValue(ext, EXT_PREBID_TYPE_REFERENCE)) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(IllegalArgumentException::new); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badServerResponse("Failed to get type of bid \"%s\"".formatted(bid.getImpid()))); + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/axis/AxisBidder.java b/src/main/java/org/prebid/server/bidder/axis/AxisBidder.java index f038aec67fe..d5f2a867f8c 100644 --- a/src/main/java/org/prebid/server/bidder/axis/AxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/axis/AxisBidder.java @@ -53,7 +53,7 @@ public Result>> makeHttpRequests(BidRequest request } catch (PreBidException e) { continue; } - httpRequests.add(makeRequest(request, imp, extImpAxis)); + httpRequests.add(makeRequest(request, imp)); } return Result.withValues(httpRequests); @@ -67,7 +67,7 @@ private ExtImpAxis parseImpExt(Imp imp) { } } - private HttpRequest makeRequest(BidRequest bidRequest, Imp imp, ExtImpAxis extImpAxis) { + private HttpRequest makeRequest(BidRequest bidRequest, Imp imp) { final BidRequest modifyBidRequest = bidRequest.toBuilder() .imp(Collections.singletonList(imp)) .build(); diff --git a/src/main/java/org/prebid/server/bidder/axonix/AxonixBidder.java b/src/main/java/org/prebid/server/bidder/axonix/AxonixBidder.java index 7894a0bb274..7bbc9cb1463 100644 --- a/src/main/java/org/prebid/server/bidder/axonix/AxonixBidder.java +++ b/src/main/java/org/prebid/server/bidder/axonix/AxonixBidder.java @@ -49,7 +49,7 @@ public AxonixBidder(String endpointUrl, JacksonMapper mapper) { public Result>> makeHttpRequests(BidRequest request) { final ExtImpAxonix extImpAxonix; try { - extImpAxonix = parseImpExt(request.getImp().get(0)); + extImpAxonix = parseImpExt(request.getImp().getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } diff --git a/src/main/java/org/prebid/server/bidder/beachfront/BeachfrontBidder.java b/src/main/java/org/prebid/server/bidder/beachfront/BeachfrontBidder.java index 5ea46e61611..f9bdfa88e78 100644 --- a/src/main/java/org/prebid/server/bidder/beachfront/BeachfrontBidder.java +++ b/src/main/java/org/prebid/server/bidder/beachfront/BeachfrontBidder.java @@ -152,7 +152,7 @@ private String resolveVideoUri(String appId, Boolean isPrebid) { private static boolean checkFormats(Banner banner) { final List formats = banner != null ? banner.getFormat() : null; - final Format firstFormat = CollectionUtils.isNotEmpty(formats) ? formats.get(0) : null; + final Format firstFormat = CollectionUtils.isNotEmpty(formats) ? formats.getFirst() : null; final boolean isHeightNonZero = firstFormat != null && !Objects.equals(firstFormat.getH(), 0); final boolean isWidthNonZero = firstFormat != null && !Objects.equals(firstFormat.getW(), 0); return isHeightNonZero && isWidthNonZero; @@ -207,7 +207,7 @@ private BeachfrontBannerRequest getBannerRequest(BidRequest bidRequest, } final Site site = bidRequest.getSite(); - final Integer firstImpSecure = bannerImps.get(0).getSecure(); + final Integer firstImpSecure = bannerImps.getFirst().getSecure(); if (site != null) { final String page = site.getPage(); @@ -503,7 +503,7 @@ private List processVideoResponse(String responseBody, HttpRequest bids = bidResponse.getSeatbid().get(0).getBid(); + final List bids = bidResponse.getSeatbid().getFirst().getBid(); final List updatedBids = httpRequest.getUri().contains(NURL_VIDEO_ENDPOINT_SUFFIX) ? updateNurlVideoBids(bids, videoRequest.getRequest().getImp()) : updateVideoBids(bids); @@ -556,7 +556,7 @@ private BidderBid updateBidderBid(BidderBid bidderBid) { } final List cat = bid.getCat(); - final String primaryCategory = CollectionUtils.isNotEmpty(cat) ? cat.get(0) : null; + final String primaryCategory = CollectionUtils.isNotEmpty(cat) ? cat.getFirst() : null; final Bid resolvedBid = bid.toBuilder().ext(resolveBidExt(duration, primaryCategory)).build(); return BidderBid.of(resolvedBid, bidderBid.getType(), bidderBid.getBidCurrency()); diff --git a/src/main/java/org/prebid/server/bidder/beintoo/BeintooBidder.java b/src/main/java/org/prebid/server/bidder/beintoo/BeintooBidder.java index 0b8af084ada..eb3eb6a27e7 100644 --- a/src/main/java/org/prebid/server/bidder/beintoo/BeintooBidder.java +++ b/src/main/java/org/prebid/server/bidder/beintoo/BeintooBidder.java @@ -141,7 +141,7 @@ private static Banner modifyImpBanner(Banner banner) { final List formatSkipFirst = originalFormat.subList(1, originalFormat.size()); bannerBuilder.format(formatSkipFirst); - final Format firstFormat = originalFormat.get(0); + final Format firstFormat = originalFormat.getFirst(); bannerBuilder.w(firstFormat.getW()); bannerBuilder.h(firstFormat.getH()); diff --git a/src/main/java/org/prebid/server/bidder/between/BetweenBidder.java b/src/main/java/org/prebid/server/bidder/between/BetweenBidder.java index 101fb1f75a5..a795a3efbb9 100644 --- a/src/main/java/org/prebid/server/bidder/between/BetweenBidder.java +++ b/src/main/java/org/prebid/server/bidder/between/BetweenBidder.java @@ -119,7 +119,7 @@ private static Imp modifyImp(Imp imp, Integer secure) { private static Banner resolveBanner(Banner banner) { if (banner.getW() == null && banner.getH() == null) { final List bannerFormat = banner.getFormat(); - final Format firstFormat = bannerFormat.get(0); + final Format firstFormat = bannerFormat.getFirst(); final List formatSkipFirst = bannerFormat.subList(1, bannerFormat.size()); return banner.toBuilder() .format(formatSkipFirst) diff --git a/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java new file mode 100644 index 00000000000..d425ebb9572 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bidmatic.ExtImpBidmatic; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class BidmaticBidder implements Bidder { + + private static final TypeReference> EXT_IMP_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BidmaticBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final Map> sourceToImpsMap = new HashMap<>(); + + for (Imp imp : request.getImp()) { + final ExtImpBidmatic extImp; + try { + extImp = parseImpExt(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + + final int sourceId; + try { + sourceId = Integer.parseInt(extImp.getSourceId()); + } catch (NumberFormatException e) { + errors.add(BidderError.badInput("Cannot parse sourceId=%s to int".formatted(extImp.getSourceId()))); + continue; + } + + final Imp modifiedImp = modifyImp(imp, sourceId, extImp); + sourceToImpsMap.putIfAbsent(sourceId, new ArrayList<>()); + sourceToImpsMap.get(sourceId).add(modifiedImp); + } + + if (sourceToImpsMap.isEmpty()) { + return Result.withErrors(errors); + } + + sourceToImpsMap.forEach((sourceId, imps) -> requests.add(makeHttpRequest(request, sourceId, imps))); + return Result.of(requests, errors); + } + + private ExtImpBidmatic parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_IMP_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, Integer sourceId, ExtImpBidmatic extImp) { + final BidmaticImpExt modifiedExtImp = BidmaticImpExt.of( + sourceId, extImp.getPlacementId(), extImp.getSiteId(), extImp.getBidFloor()); + + return imp.toBuilder() + .bidfloor(BidderUtil.isValidPrice(extImp.getBidFloor()) ? extImp.getBidFloor() : imp.getBidfloor()) + .ext(mapper.mapper().createObjectNode().set("bidmatic", mapper.mapper().valueToTree(modifiedExtImp))) + .build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, Integer sourceId, List imps) { + final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); + return BidderUtil.defaultRequest(modifiedRequest, makeUrl(sourceId), mapper); + } + + private String makeUrl(Integer sourceId) { + return endpointUrl + "?source=%d".formatted(sourceId); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final Map impMap = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, impMap, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, Map impMap, String currency, List errors) { + try { + final Pair bidType = getBidType(bid, impMap); + final Bid modifiedBid = bid.toBuilder().mtype(bidType.getRight()).build(); + return BidderBid.of(modifiedBid, bidType.getLeft(), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static Pair getBidType(Bid bid, Map impIdToImpMap) { + final Imp imp = impIdToImpMap.get(bid.getImpid()); + if (imp == null) { + throw new PreBidException("ignoring bid id=%s, request doesn't contain any impression with id=%s" + .formatted(bid.getId(), bid.getImpid())); + } + + if (imp.getBanner() != null) { + return Pair.of(BidType.banner, 1); + } else if (imp.getVideo() != null) { + return Pair.of(BidType.video, 2); + } else if (imp.getXNative() != null) { + return Pair.of(BidType.xNative, 4); + } else if (imp.getAudio() != null) { + return Pair.of(BidType.audio, 3); + } else { + return Pair.of(BidType.banner, 1); + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java new file mode 100644 index 00000000000..eead679d233 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java @@ -0,0 +1,22 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class BidmaticImpExt { + + @JsonProperty("source") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; +} diff --git a/src/main/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidder.java b/src/main/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidder.java index ce06699d970..e871390d01e 100644 --- a/src/main/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidder.java +++ b/src/main/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidder.java @@ -90,13 +90,13 @@ private BidderBid extractBids(BidResponse bidResponse) { } private BidderBid bidsFromResponse(BidResponse bidResponse) { - final List bids = bidResponse.getSeatbid().get(0).getBid(); + final List bids = bidResponse.getSeatbid().getFirst().getBid(); if (CollectionUtils.isEmpty(bids)) { throw new PreBidException("Empty SeatBid.Bids"); } - final Bid bid = bids.get(0); + final Bid bid = bids.getFirst(); return BidderBid.of(bid, getBidType(bid.getExt()), bidResponse.getCur()); } diff --git a/src/main/java/org/prebid/server/bidder/bidstack/BidstackBidder.java b/src/main/java/org/prebid/server/bidder/bidstack/BidstackBidder.java index f1b9613ebc8..0a3997e9074 100644 --- a/src/main/java/org/prebid/server/bidder/bidstack/BidstackBidder.java +++ b/src/main/java/org/prebid/server/bidder/bidstack/BidstackBidder.java @@ -28,8 +28,8 @@ import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collections; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -130,7 +130,8 @@ private BigDecimal convertBidFloorCurrency(BigDecimal bidFloor, } private MultiMap constructHeaders(BidRequest bidRequest) { - final String publishedId = StringUtils.defaultString(parseExtImp(bidRequest.getImp().get(0)).getPublisherId()); + final String publishedId = StringUtils.defaultString( + parseExtImp(bidRequest.getImp().getFirst()).getPublisherId()); return HttpUtil.headers() .add(HttpUtil.AUTHORIZATION_HEADER.toString(), "Bearer " + publishedId); } diff --git a/src/main/java/org/prebid/server/bidder/bigoad/BigoadBidder.java b/src/main/java/org/prebid/server/bidder/bigoad/BigoadBidder.java new file mode 100644 index 00000000000..7c24e7cf480 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bigoad/BigoadBidder.java @@ -0,0 +1,150 @@ +package org.prebid.server.bidder.bigoad; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bigoad.ExtImpBigoad; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class BigoadBidder implements Bidder { + + private static final TypeReference> BIGOAD_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String SSP_ID_MACRO = "{{SspId}}"; + private static final String OPEN_RTB_VERSION = "2.5"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BigoadBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(request.getImp()); + + final Imp firstImp = modifiedImps.getFirst(); + final ExtImpBigoad extImpBigoad; + try { + extImpBigoad = parseImpExt(firstImp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + modifiedImps.set(0, modifyImp(firstImp, extImpBigoad)); + return Result.withValue(BidderUtil.defaultRequest( + updateBidRequest(request, modifiedImps), + headers(), + makeEndpointUrl(extImpBigoad), + mapper)); + } + + private ExtImpBigoad parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), BIGOAD_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("imp %s: unable to unmarshal ext.bidder: %s" + .formatted(imp.getId(), e.getMessage())); + } + } + + private Imp modifyImp(Imp imp, ExtImpBigoad extImpBigoad) { + return imp.toBuilder().ext(mapper.mapper().valueToTree(extImpBigoad)).build(); + } + + private static BidRequest updateBidRequest(BidRequest bidRequest, List imps) { + return bidRequest.toBuilder().imp(imps).build(); + } + + private static MultiMap headers() { + return HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, OPEN_RTB_VERSION); + } + + private String makeEndpointUrl(ExtImpBigoad extImpBigoadx) { + final String safeSspId = HttpUtil.encodeUrl(StringUtils.trimToEmpty(extImpBigoadx.getSspId())); + return endpointUrl.replace(SSP_ID_MACRO, safeSspId); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = parseBidResponse(httpCall.getResponse()); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private BidResponse parseBidResponse(HttpResponse response) { + try { + return mapper.decodeValue(response.getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException("Bad server response: " + e.getMessage()); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType; + try { + bidType = resolveBidType(bid.getMtype(), bid.getImpid()); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, currency); + } + + private static BidType resolveBidType(Integer mType, String impId) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unrecognized bid type in response from bigoad " + impId); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java b/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java deleted file mode 100644 index cf0278d4219..00000000000 --- a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.prebid.server.bidder.bizzclick; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.HttpResponse; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.bizzclick.ExtImpBizzclick; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.HttpUtil; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class BizzclickBidder implements Bidder { - - private static final TypeReference> BIZZCLICK_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; - private static final String URL_SOURCE_ID_MACRO = "{{.SourceId}}"; - private static final String URL_ACCOUNT_ID_MACRO = "{{.AccountID}}"; - private static final String DEFAULT_CURRENCY = "USD"; - - private final String endpointUrl; - private final JacksonMapper mapper; - - public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest request) { - final List imps = request.getImp(); - final ExtImpBizzclick extImpBizzclick; - try { - extImpBizzclick = parseImpExt(imps.get(0)); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - - final List modifiedImps = imps.stream() - .map(BizzclickBidder::modifyImp) - .toList(); - - return Result.withValue(createHttpRequest(request, modifiedImps, extImpBizzclick)); - } - - private ExtImpBizzclick parseImpExt(Imp imp) throws PreBidException { - try { - return mapper.mapper().convertValue(imp.getExt(), BIZZCLICK_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException("ext.bidder not provided"); - } - } - - private static Imp modifyImp(Imp imp) { - return imp.toBuilder().ext(null).build(); - } - - private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBizzclick ext) { - final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); - - return HttpRequest.builder() - .method(HttpMethod.POST) - .headers(headers(modifiedRequest.getDevice())) - .uri(buildEndpointUrl(ext)) - .body(mapper.encodeToBytes(modifiedRequest)) - .payload(modifiedRequest) - .build(); - } - - private static MultiMap headers(Device device) { - final MultiMap headers = HttpUtil.headers() - .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); - - if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); - } - - return headers; - } - - private String buildEndpointUrl(ExtImpBizzclick ext) { - return endpointUrl.replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(ext.getPlacementId())) - .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(ext.getAccountId())); - } - - @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = parseBidResponse(httpCall.getResponse()); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); - } catch (PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private BidResponse parseBidResponse(HttpResponse response) { - try { - return mapper.decodeValue(response.getBody(), BidResponse.class); - } catch (DecodeException e) { - throw new PreBidException("Bad server response."); - } - } - - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - throw new PreBidException("Empty SeatBid array"); - } - - final SeatBid seatBid = bidResponse.getSeatbid().get(0); - if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) { - return Collections.emptyList(); - } - - return seatBid.getBid().stream() - .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), DEFAULT_CURRENCY)) - .toList(); - } - - private static BidType resolveBidType(String impId, List imps) { - for (Imp imp : imps) { - if (Objects.equals(impId, imp.getId())) { - if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getXNative() != null) { - return BidType.xNative; - } - break; - } - } - return BidType.banner; - } -} diff --git a/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java new file mode 100644 index 00000000000..c317935419b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java @@ -0,0 +1,156 @@ +package org.prebid.server.bidder.blasto; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class BlastoBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; + private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; + private static final String DEFAULT_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BlastoBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List imps = request.getImp(); + final ExtImpBlasto extImp; + try { + extImp = parseImpExt(imps.getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final List modifiedImps = imps.stream() + .map(BlastoBidder::modifyImp) + .toList(); + + return Result.withValue(createHttpRequest(request, modifiedImps, extImp)); + } + + private ExtImpBlasto parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("ext.bidder not provided"); + } + } + + private static Imp modifyImp(Imp imp) { + return imp.toBuilder().ext(null).build(); + } + + private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBlasto ext) { + final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .headers(headers(modifiedRequest.getDevice())) + .uri(buildEndpointUrl(ext)) + .body(mapper.encodeToBytes(modifiedRequest)) + .payload(modifiedRequest) + .build(); + } + + private static MultiMap headers(Device device) { + final MultiMap headers = HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } + + return headers; + } + + private String buildEndpointUrl(ExtImpBlasto extImp) { + return endpointUrl + .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId())) + .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId())); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = parseBidResponse(httpCall.getResponse()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private BidResponse parseBidResponse(HttpResponse response) { + try { + return mapper.decodeValue(response.getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException("Bad server response."); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + + final SeatBid seatBid = bidResponse.getSeatbid().getFirst(); + if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) { + return Collections.emptyList(); + } + + return seatBid.getBid().stream() + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), DEFAULT_CURRENCY)) + .toList(); + } + + private static BidType resolveBidType(String impId, List imps) { + for (Imp imp : imps) { + if (Objects.equals(impId, imp.getId())) { + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + break; + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/bidder/bliink/BliinkBidder.java b/src/main/java/org/prebid/server/bidder/bliink/BliinkBidder.java index a66883f4730..18fe5bd5d3a 100644 --- a/src/main/java/org/prebid/server/bidder/bliink/BliinkBidder.java +++ b/src/main/java/org/prebid/server/bidder/bliink/BliinkBidder.java @@ -76,7 +76,7 @@ private static List extractBids(BidRequest bidRequest, return Collections.emptyList(); } - return Optional.ofNullable(bidResponse.getSeatbid().get(0)) + return Optional.ofNullable(bidResponse.getSeatbid().getFirst()) .map(SeatBid::getBid) .orElseGet(Collections::emptyList) .stream() diff --git a/src/main/java/org/prebid/server/bidder/bluesea/BlueSeaBidder.java b/src/main/java/org/prebid/server/bidder/bluesea/BlueSeaBidder.java index 90bd3537eb1..8c93ba9f73d 100644 --- a/src/main/java/org/prebid/server/bidder/bluesea/BlueSeaBidder.java +++ b/src/main/java/org/prebid/server/bidder/bluesea/BlueSeaBidder.java @@ -32,11 +32,9 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; public class BlueSeaBidder implements Bidder { - private static final Set SUPPORTED_BID_TYPES_TEXTUAL = Set.of("banner", "video", "native"); private static final TypeReference> BLUE_SEA_EXT_TYPE_REFERENCE = new TypeReference<>() { }; diff --git a/src/main/java/org/prebid/server/bidder/boldwin/BoldwinBidder.java b/src/main/java/org/prebid/server/bidder/boldwin/BoldwinBidder.java index d0c702d42bf..0ddb3be524d 100644 --- a/src/main/java/org/prebid/server/bidder/boldwin/BoldwinBidder.java +++ b/src/main/java/org/prebid/server/bidder/boldwin/BoldwinBidder.java @@ -129,7 +129,7 @@ private static List extractBids(BidResponse bidResponse) { } private static BidType getBidType(Bid bid) { - final Integer mType = bid.getMtype() != null ? bid.getMtype() : 999; + final int mType = bid.getMtype() != null ? bid.getMtype() : 999; return switch (mType) { case 1 -> BidType.banner; case 2 -> BidType.video; diff --git a/src/main/java/org/prebid/server/bidder/brave/BraveBidder.java b/src/main/java/org/prebid/server/bidder/brave/BraveBidder.java index 7260d454584..0819e42479a 100644 --- a/src/main/java/org/prebid/server/bidder/brave/BraveBidder.java +++ b/src/main/java/org/prebid/server/bidder/brave/BraveBidder.java @@ -50,7 +50,7 @@ public Result>> makeHttpRequests(BidRequest request final String url; try { - final ExtImpBrave extImpBrave = parseImpExt(request.getImp().get(0)); + final ExtImpBrave extImpBrave = parseImpExt(request.getImp().getFirst()); url = resolveEndpoint(extImpBrave.getPlacementId()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); diff --git a/src/main/java/org/prebid/server/bidder/bwx/BwxBidder.java b/src/main/java/org/prebid/server/bidder/bwx/BwxBidder.java new file mode 100644 index 00000000000..0bd006ff249 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bwx/BwxBidder.java @@ -0,0 +1,135 @@ +package org.prebid.server.bidder.bwx; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bwx.ExtImpBwx; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class BwxBidder implements Bidder { + + private static final TypeReference> BWX_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String PUBLISHER_ID_MACRO = "{{SourceId}}"; + private final String endpointUrl; + private final JacksonMapper mapper; + + public BwxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpBwx extImpBwx; + try { + extImpBwx = parseImpExt(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + httpRequests.add(createHttpRequest(request, extImpBwx)); + } + + return Result.of(httpRequests, errors); + } + + private ExtImpBwx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), BWX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private HttpRequest createHttpRequest(BidRequest request, ExtImpBwx extImpBwx) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveEndpoint(extImpBwx)) + .body(mapper.encodeToBytes(request)) + .payload(request) + .headers(HttpUtil.headers()) + .build(); + } + + private String resolveEndpoint(ExtImpBwx extImpBwx) { + return endpointUrl + .replace(URL_HOST_MACRO, StringUtils.defaultString(extImpBwx.getEnv())) + .replace(PUBLISHER_ID_MACRO, StringUtils.defaultString(extImpBwx.getPid())); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (PreBidException | DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidsFromResponse(bidResponse); + } + + private List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .filter(Objects::nonNull) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Failed to parse bid mtype: %s for impression id %s".formatted(bid.getMtype(), bid.getImpid()) + ); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/ccx/CcxBidder.java b/src/main/java/org/prebid/server/bidder/ccx/CcxBidder.java deleted file mode 100644 index 91fcdc71a1c..00000000000 --- a/src/main/java/org/prebid/server/bidder/ccx/CcxBidder.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.prebid.server.bidder.ccx; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class CcxBidder implements Bidder { - - private final String endpointUrl; - private final JacksonMapper mapper; - - public CcxBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest request) { - - return Result.withValue(BidderUtil.defaultRequest(request, endpointUrl, mapper)); - } - - @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); - } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - return bidsFromResponse(bidRequest, bidResponse); - } - - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid, bidRequest.getImp()), bidResponse.getCur())) - .toList(); - } - - private static BidType getBidType(Bid bid, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(bid.getImpid())) { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } - break; - } - } - return BidType.banner; - } - -} diff --git a/src/main/java/org/prebid/server/bidder/cointraffic/CointrafficBidder.java b/src/main/java/org/prebid/server/bidder/cointraffic/CointrafficBidder.java new file mode 100644 index 00000000000..8f17681295c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/cointraffic/CointrafficBidder.java @@ -0,0 +1,69 @@ +package org.prebid.server.bidder.cointraffic; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class CointrafficBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public CointrafficBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, headers, endpointUrl, mapper)); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/concert/ConcertBidder.java b/src/main/java/org/prebid/server/bidder/concert/ConcertBidder.java new file mode 100644 index 00000000000..aaadcb08a89 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/concert/ConcertBidder.java @@ -0,0 +1,155 @@ +package org.prebid.server.bidder.concert; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.concert.ExtImpConcert; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class ConcertBidder implements Bidder { + + private static final TypeReference> CONCERT_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String ADAPTER_VERSION = "1.0.0"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ConcertBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + try { + final ExtImpConcert extImpConcert = parseImpExt(request.getImp().getFirst()); + return Result.withValue(BidderUtil.defaultRequest( + updateBidRequest(request, extImpConcert), + endpointUrl, + mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpConcert parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), CONCERT_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("get bidder ext: bidder ext: " + e.getMessage()); + } + } + + private BidRequest updateBidRequest(BidRequest bidRequest, ExtImpConcert extImpConcert) { + return bidRequest.toBuilder() + .ext(updateExtRequest(bidRequest.getExt(), extImpConcert)) + .build(); + } + + private ExtRequest updateExtRequest(ExtRequest extRequest, ExtImpConcert extImpConcert) { + final ExtRequest newExtRequest = extRequest != null + ? copyExtRequest(extRequest) + : ExtRequest.of(null); + + final String partnerId = extImpConcert.getPartnerId(); + newExtRequest.addProperty("adapterVersion", TextNode.valueOf(ADAPTER_VERSION)); + if (partnerId != null) { + newExtRequest.addProperty("partnerId", TextNode.valueOf(partnerId)); + } + + return newExtRequest; + } + + private ExtRequest copyExtRequest(ExtRequest extRequest) { + try { + return mapper.mapper().treeToValue(mapper.mapper().valueToTree(extRequest), ExtRequest.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = parseBidResponse(httpCall.getResponse()); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private BidResponse parseBidResponse(HttpResponse response) { + try { + return mapper.decodeValue(response.getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("no bids returned"); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType; + try { + bidType = resolveBidType(bid.getMtype(), bid.getImpid()); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, currency); + } + + private static BidType resolveBidType(Integer mType, String impId) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> throw new PreBidException("native media types are not yet supported"); + case null, default -> + throw new PreBidException("Failed to parse media type for bid: \"%s\"".formatted(impId)); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java new file mode 100644 index 00000000000..4c64992184b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.connectad; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.connectad.ExtImpConnectAd; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ConnectAdBidder implements Bidder { + + private static final TypeReference> CONNECTAD_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String HTTPS_PREFIX = "https"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ConnectAdBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final int secure = secureFrom(request.getSite()); + + final List errors = new ArrayList<>(); + final List processedImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpConnectAd impExt = parseImpExt(imp); + final Imp updatedImp = updateImp(imp, secure, impExt.getSiteId(), impExt.getBidFloor()); + processedImps.add(updatedImp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + if (CollectionUtils.isNotEmpty(errors)) { + errors.add(BidderError.badInput("Error in preprocess of Imp")); + return Result.withErrors(errors); + } + final BidRequest outgoingRequest = request.toBuilder().imp(processedImps).build(); + + return Result.of( + Collections.singletonList(BidderUtil.defaultRequest( + outgoingRequest, + resolveHeaders(outgoingRequest.getDevice()), + endpointUrl, + mapper)), + errors); + } + + private static int secureFrom(Site site) { + final String page = site != null ? site.getPage() : null; + return page != null && page.startsWith(HTTPS_PREFIX) ? 1 : 0; + } + + private ExtImpConnectAd parseImpExt(Imp imp) { + final ExtImpConnectAd extImpConnectAd; + try { + extImpConnectAd = mapper.mapper().convertValue(imp.getExt(), CONNECTAD_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Impression id=%s, has invalid Ext".formatted(imp.getId())); + } + final String siteId = extImpConnectAd.getSiteId(); + if (siteId == null) { + throw new PreBidException("Impression id=%s, has no siteId present".formatted(imp.getId())); + } + return extImpConnectAd; + } + + private Imp updateImp(Imp imp, Integer secure, String siteId, BigDecimal bidFloor) { + final boolean isValidBidFloor = BidderUtil.isValidPrice(bidFloor); + return imp.toBuilder() + .banner(updateBanner(imp.getBanner())) + .tagid(siteId) + .secure(secure) + .bidfloor(isValidBidFloor ? bidFloor : imp.getBidfloor()) + .bidfloorcur(isValidBidFloor ? "USD" : imp.getBidfloorcur()) + .build(); + } + + private static Banner updateBanner(Banner banner) { + if (banner == null) { + throw new PreBidException("We need a Banner Object in the request"); + } + + if (banner.getW() != null || banner.getH() != null) { + return banner; + } + + final List formats = banner.getFormat(); + if (CollectionUtils.isEmpty(formats)) { + throw new PreBidException("At least one size is required"); + } + + final Format firstFormat = formats.getFirst(); + final List slicedFormats = new ArrayList<>(formats); + slicedFormats.removeFirst(); + + return banner.toBuilder() + .format(slicedFormats) + .w(firstFormat.getW()) + .h(firstFormat.getH()) + .build(); + } + + private static MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + + final Integer dnt = device.getDnt(); + headers.add(HttpUtil.DNT_HEADER, dnt != null ? dnt.toString() : "0"); + } + return headers; + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/connectad/ConnectadBidder.java b/src/main/java/org/prebid/server/bidder/connectad/ConnectadBidder.java deleted file mode 100644 index 6ff542fefe1..00000000000 --- a/src/main/java/org/prebid/server/bidder/connectad/ConnectadBidder.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.prebid.server.bidder.connectad; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.connectad.ExtImpConnectAd; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class ConnectadBidder implements Bidder { - - private static final TypeReference> CONNECTAD_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; - private static final String HTTPS_PREFIX = "https"; - - private final String endpointUrl; - private final JacksonMapper mapper; - - public ConnectadBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest request) { - final int secure = secureFrom(request.getSite()); - - final List errors = new ArrayList<>(); - final List processedImps = new ArrayList<>(); - - for (Imp imp : request.getImp()) { - try { - final ExtImpConnectAd impExt = parseImpExt(imp); - final Imp updatedImp = updateImp(imp, secure, impExt.getSiteId(), impExt.getBidfloor()); - processedImps.add(updatedImp); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - } - if (CollectionUtils.isNotEmpty(errors)) { - errors.add(BidderError.badInput("Error in preprocess of Imp")); - return Result.withErrors(errors); - } - final BidRequest outgoingRequest = request.toBuilder().imp(processedImps).build(); - - return Result.of(Collections.singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(resolveHeaders(request.getDevice())) - .payload(outgoingRequest) - .body(mapper.encodeToBytes(outgoingRequest)) - .build()), - errors); - } - - private static int secureFrom(Site site) { - final String page = site != null ? site.getPage() : null; - return page != null && page.startsWith(HTTPS_PREFIX) ? 1 : 0; - } - - private ExtImpConnectAd parseImpExt(Imp imp) { - final ExtImpConnectAd extImpConnectAd; - try { - extImpConnectAd = mapper.mapper().convertValue(imp.getExt(), CONNECTAD_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException("Impression id=%s, has invalid Ext".formatted(imp.getId())); - } - final Integer siteId = extImpConnectAd.getSiteId(); - if (siteId == null || siteId.equals(0)) { - throw new PreBidException("Impression id=%s, has no siteId present".formatted(imp.getId())); - } - return extImpConnectAd; - } - - private Imp updateImp(Imp imp, Integer secure, Integer siteId, BigDecimal bidFloor) { - final Imp.ImpBuilder updatedImp = imp.toBuilder().tagid(siteId.toString()).secure(secure); - - if (BidderUtil.isValidPrice(bidFloor)) { - updatedImp.bidfloor(bidFloor).bidfloorcur("USD"); - } - - final Banner banner = imp.getBanner(); - if (banner == null) { - throw new PreBidException("We need a Banner Object in the request"); - } - - if (banner.getW() == null && banner.getH() == null) { - if (CollectionUtils.isEmpty(banner.getFormat())) { - throw new PreBidException("At least one size is required"); - } - final Format format = banner.getFormat().get(0); - final List slicedFormatList = new ArrayList<>(banner.getFormat()); - - slicedFormatList.remove(0); - updatedImp.banner(banner.toBuilder().format(slicedFormatList).w(format.getW()).h(format.getH()).build()); - } - - return updatedImp.build(); - } - - private static MultiMap resolveHeaders(Device device) { - final MultiMap headers = HttpUtil.headers(); - - if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); - - final Integer dnt = device.getDnt(); - headers.add(HttpUtil.DNT_HEADER, dnt != null ? dnt.toString() : "0"); - } - return headers; - } - - @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(bidResponse), Collections.emptyList()); - } catch (DecodeException | PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private List extractBids(BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - return bidsFromResponse(bidResponse); - } - - private List bidsFromResponse(BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) - .toList(); - } -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/ConsumableBidder.java b/src/main/java/org/prebid/server/bidder/consumable/ConsumableBidder.java index 507fcc97666..344680b9aa4 100644 --- a/src/main/java/org/prebid/server/bidder/consumable/ConsumableBidder.java +++ b/src/main/java/org/prebid/server/bidder/consumable/ConsumableBidder.java @@ -1,227 +1,217 @@ package org.prebid.server.bidder.consumable; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.consumable.model.ConsumableAdType; -import org.prebid.server.bidder.consumable.model.ConsumableBidGdpr; -import org.prebid.server.bidder.consumable.model.ConsumableBidRequest; -import org.prebid.server.bidder.consumable.model.ConsumableBidResponse; -import org.prebid.server.bidder.consumable.model.ConsumableDecision; -import org.prebid.server.bidder.consumable.model.ConsumablePlacement; -import org.prebid.server.bidder.consumable.model.ConsumablePricing; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtRegs; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.consumable.ExtImpConsumable; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import java.math.BigDecimal; -import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; +import java.util.Optional; -public class ConsumableBidder implements Bidder { +public class ConsumableBidder implements Bidder { - private final String endpointUrl; + private static final String OPENRTB_VERSION = "2.5"; + private static final TypeReference> CONS_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + public static final String SITE_URI_PATH = "/sb/rtb"; + public static final String APP_URI_PATH = "/rtb/bid?s="; private final JacksonMapper mapper; + private final String endpointUrl; + public ConsumableBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @Override - public Result>> makeHttpRequests(BidRequest request) { - final ConsumableBidRequest.ConsumableBidRequestBuilder requestBuilder = ConsumableBidRequest.builder() - .time(Instant.now().getEpochSecond()) - .includePricingData(true) - .enableBotFiltering(true) - .parallel(true); - - final Site site = request.getSite(); - if (site != null) { - requestBuilder - .referrer(site.getRef()) - .url(site.getPage()); - } - - final Regs regs = request.getRegs(); - - final String gpp = regs != null ? regs.getGpp() : null; - if (gpp != null) { - requestBuilder.gpp(gpp); - } - - final List gppSid = regs != null ? regs.getGppSid() : null; - if (CollectionUtils.isNotEmpty(gppSid)) { - requestBuilder.gppSid(gppSid); - } + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List imps = new ArrayList<>(); + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + String placementId = null; + for (Imp imp : bidRequest.getImp()) { + try { + final ExtImpConsumable impExt = parseImpExt(imp); + if (!isImpValid(bidRequest.getSite(), bidRequest.getApp(), impExt)) { + continue; + } + if (Strings.isNullOrEmpty(placementId) && !Strings.isNullOrEmpty(impExt.getPlacementId())) { + placementId = impExt.getPlacementId(); + } - final ExtRegs extRegs = regs != null ? regs.getExt() : null; - final String usPrivacy = extRegs != null ? extRegs.getUsPrivacy() : null; - if (usPrivacy != null) { - requestBuilder.usPrivacy(usPrivacy); - } + imps.add(imp); - final Integer gdpr = extRegs != null ? extRegs.getGdpr() : null; - final User user = request.getUser(); - final ExtUser extUser = user != null ? user.getExt() : null; - final String gdprConsent = extUser != null ? extUser.getConsent() : null; - if (gdpr != null || gdprConsent != null) { - final ConsumableBidGdpr.ConsumableBidGdprBuilder bidGdprBuilder = ConsumableBidGdpr.builder(); - if (gdpr != null) { - bidGdprBuilder.applies(gdpr != 0); - } - if (gdprConsent != null) { - bidGdprBuilder.consent(gdprConsent).build(); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); } - requestBuilder.gdpr(bidGdprBuilder.build()); } - - try { - resolveRequestFields(requestBuilder, request.getImp()); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); + if (imps.isEmpty()) { + return Result.withErrors(errors); } - - final ConsumableBidRequest outgoingRequest = requestBuilder.build(); - - return Result.withValue(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .body(mapper.encodeToBytes(outgoingRequest)) - .headers(resolveHeaders(request)) - .payload(outgoingRequest) - .build()); - } - - private void resolveRequestFields(ConsumableBidRequest.ConsumableBidRequestBuilder requestBuilder, - List imps) { - final List placements = new ArrayList<>(); - for (int i = 0; i < imps.size(); i++) { - final Imp currentImp = imps.get(i); - final ExtImpConsumable extImpConsumable = parseImpExt(currentImp); - if (i == 0) { - requestBuilder - .networkId(extImpConsumable.getNetworkId()) - .siteId(extImpConsumable.getSiteId()) - .unitId(extImpConsumable.getUnitId()) - .unitName(extImpConsumable.getUnitName()); - } - placements.add(ConsumablePlacement.builder() - .divName(currentImp.getId()) - .networkId(extImpConsumable.getNetworkId()) - .siteId(extImpConsumable.getSiteId()) - .unitId(extImpConsumable.getUnitId()) - .unitName(extImpConsumable.getUnitName()) - .adTypes(ConsumableAdType.getSizeCodes(currentImp.getBanner().getFormat())) - .build()); - } - requestBuilder.placements(placements); + final BidRequest modRequest = modifyBidRequest(bidRequest, imps); + final String finalUrl = constructUri(placementId); + httpRequests.add(BidderUtil.defaultRequest(modRequest, resolveHeaders(), finalUrl, mapper)); + return Result.of(httpRequests, errors); } private ExtImpConsumable parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt().get("bidder"), ExtImpConsumable.class); + return mapper.mapper().convertValue(imp.getExt(), CONS_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(e.getMessage()); } } - private static MultiMap resolveHeaders(BidRequest request) { - final MultiMap headers = HttpUtil.headers(); + private boolean isImpValid(Site site, App app, ExtImpConsumable impExt) { + return (app != null && !Strings.isNullOrEmpty(impExt.getPlacementId())) + || (site != null && impExt.getSiteId() != 0 && impExt.getNetworkId() != 0 && impExt.getUnitId() != 0); - final Device device = request.getDevice(); - if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); - final String ip = device.getIp(); - if (StringUtils.isNotBlank(ip)) { - headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip); - headers.add("Forwarded", "for=" + ip); - } - } + } - final User user = request.getUser(); - if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { - headers.add(HttpUtil.COOKIE_HEADER, "azk=" + user.getBuyeruid().trim()); - } + private BidRequest modifyBidRequest(BidRequest bidRequest, List imps) { + return bidRequest.toBuilder().imp(imps).build(); + } - final Site site = request.getSite(); - final String page = site != null ? site.getPage() : null; - if (StringUtils.isNotBlank(page)) { - headers.set(HttpUtil.REFERER_HEADER, page); - try { - headers.set(HttpUtil.ORIGIN_HEADER, HttpUtil.validateUrl(page)); - } catch (IllegalArgumentException e) { - // do nothing, just skip adding this header - } - } + private String constructUri(String placementId) { + final String uri = Strings.isNullOrEmpty(placementId) ? SITE_URI_PATH : (APP_URI_PATH + placementId); + return this.endpointUrl + uri; + } + + private static MultiMap resolveHeaders() { + return HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, OPENRTB_VERSION); + } - return headers; + @Override + @Deprecated(since = "Not used, since Bidder.makeBidderResponse(...) was overridden.") + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + return Result.withError(BidderError.generic("Invalid method call")); } @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final ConsumableBidResponse consumableResponse; + public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { try { - consumableResponse = mapper.decodeValue(httpCall.getResponse().getBody(), ConsumableBidResponse.class); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List bidderErrors = new ArrayList<>(); + return CompositeBidderResponse.builder().bids(extractConsumableBids(bidRequest, bidResponse, bidderErrors)) + .errors(bidderErrors).build(); } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } - final List errors = new ArrayList<>(); - final List bidderBids = extractBids(bidRequest, consumableResponse.getDecisions()); - return Result.of(bidderBids, errors); - } - - private static List extractBids(BidRequest bidRequest, - Map impIdToDecisions) { - final List bidderBids = new ArrayList<>(); - for (Map.Entry entry : impIdToDecisions.entrySet()) { - final ConsumableDecision decision = entry.getValue(); - - if (decision != null) { - final ConsumablePricing pricing = decision.getPricing(); - if (pricing != null && pricing.getClearPrice() != null) { - final String impId = entry.getKey(); - - final Bid bid = Bid.builder() - .id(bidRequest.getId()) - .impid(impId) - .price(BigDecimal.valueOf(pricing.getClearPrice())) - .adm(CollectionUtils.isNotEmpty(decision.getContents()) - ? decision.getContents().get(0).getBody() : "") - .w(decision.getWidth()) - .h(decision.getHeight()) - .crid(String.valueOf(decision.getAdId())) - .exp(30) - .build(); - // Consumable units are always HTML, never VAST. - // From Prebid's point of view, this means that Consumable units - // are always "banners". - bidderBids.add(BidderBid.of(bid, BidType.banner, null)); + } + + private List extractConsumableBids(BidRequest bidRequest, BidResponse bidResponse, + List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> toBidderBid(bid, bidRequest, bidResponse, errors)) + .filter(Objects::nonNull) + .toList(); + + } + + private BidderBid toBidderBid(Bid bid, BidRequest bidRequest, BidResponse bidResponse, List errors) { + final BidType bidType; + try { + bidType = getBidType(bid, bidRequest.getImp()); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(bidResponse.getCur()) + .videoInfo(makeVideoInfo(bid)) + .build(); + } + + private static BidType getBidType(Bid bid, List imps) { + return getBidTypeFromMtype(bid.getMtype()) + .or(() -> getBidTypeFromExtPrebidType(bid.getExt())) + .orElseGet(() -> getBidTypeFromImp(imps, bid.getImpid())); + } + + private static Optional getBidTypeFromMtype(Integer mType) { + final BidType bidType = switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> null; + }; + + return Optional.ofNullable(bidType); + } + + private static Optional getBidTypeFromExtPrebidType(ObjectNode bidExt) { + return Optional.ofNullable(bidExt) + .map(ext -> ext.get("prebid")) + .map(prebid -> prebid.get("type")) + .map(JsonNode::asText).map(BidType::fromString); + } + + private static BidType getBidTypeFromImp(List imps, String impId) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getAudio() != null) { + return BidType.audio; } } } - return bidderBids; + throw new PreBidException("Unmatched impression id " + impId); + } + + private static ExtBidPrebidVideo makeVideoInfo(Bid bid) { + + final int duration = Optional.ofNullable(bid) + .map(Bid::getDur) + .orElse(0); + + return ExtBidPrebidVideo.of(duration, null); } } diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableAdType.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableAdType.java deleted file mode 100644 index ad9d505b2ce..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableAdType.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import com.iab.openrtb.request.Format; -import org.apache.commons.collections4.CollectionUtils; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class ConsumableAdType { - - private static final Map SIZE_MAP = new HashMap<>(); - - private ConsumableAdType() { - } - - public static List getSizeCodes(List formats) { - if (CollectionUtils.isEmpty(formats)) { - return Collections.emptyList(); - } - return formats.stream() - .map(format -> format.getW() + "x" + format.getH()) - .map(SIZE_MAP::get) - .filter(Objects::nonNull) - .toList(); - } - - static { - SIZE_MAP.put("120x90", 1); - SIZE_MAP.put("468x60", 3); - SIZE_MAP.put("728x90", 4); - SIZE_MAP.put("300x250", 5); - SIZE_MAP.put("160x600", 6); - SIZE_MAP.put("120x600", 7); - SIZE_MAP.put("300x100", 8); - SIZE_MAP.put("180x150", 9); - SIZE_MAP.put("336x280", 10); - SIZE_MAP.put("240x400", 11); - SIZE_MAP.put("234x60", 12); - SIZE_MAP.put("88x31", 13); - SIZE_MAP.put("120x60", 14); - SIZE_MAP.put("120x240", 15); - SIZE_MAP.put("125x125", 16); - SIZE_MAP.put("220x250", 17); - SIZE_MAP.put("250x90", 19); - SIZE_MAP.put("0x0", 20); - SIZE_MAP.put("200x90", 21); - SIZE_MAP.put("300x50", 22); - SIZE_MAP.put("320x50", 23); - SIZE_MAP.put("320x480", 24); - SIZE_MAP.put("185x185", 25); - SIZE_MAP.put("620x45", 26); - SIZE_MAP.put("300x125", 27); - SIZE_MAP.put("800x250", 28); - SIZE_MAP.put("970x90", 77); - SIZE_MAP.put("970x250", 123); - SIZE_MAP.put("300x600", 43); - SIZE_MAP.put("970x66", 286); - SIZE_MAP.put("970x280", 3230); - SIZE_MAP.put("486x60", 429); - SIZE_MAP.put("700x500", 374); - SIZE_MAP.put("300x1050", 934); - SIZE_MAP.put("320x100", 1578); - SIZE_MAP.put("320x250", 331); - SIZE_MAP.put("320x267", 3301); - SIZE_MAP.put("728x250", 2730); - } -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidGdpr.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidGdpr.java deleted file mode 100644 index 517c40d93d5..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidGdpr.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import lombok.Builder; -import lombok.Value; - -@Builder -@Value -public class ConsumableBidGdpr { - - Boolean applies; - - String consent; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidRequest.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidRequest.java deleted file mode 100644 index 1c2706d2c6f..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidRequest.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class ConsumableBidRequest { - - List placements; - - Long time; - - @JsonProperty("networkId") - Integer networkId; - - @JsonProperty("siteId") - Integer siteId; - - @JsonProperty("unitId") - Integer unitId; - - @JsonProperty("unitName") - String unitName; - - @JsonProperty("includePricingData") - Boolean includePricingData; - - ConsumableUser user; - - String referrer; - - String ip; - - String url; - - @JsonProperty("enableBotFiltering") - Boolean enableBotFiltering; - - Boolean parallel; - - String usPrivacy; - - ConsumableBidGdpr gdpr; - - String gpp; - - @JsonProperty("gpp_sid") - List gppSid; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidResponse.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidResponse.java deleted file mode 100644 index cf9e31ec3f5..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableBidResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.Map; - -@AllArgsConstructor(staticName = "of") -@Value -public class ConsumableBidResponse { - - Map decisions; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableContents.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableContents.java deleted file mode 100644 index e5e24034cc0..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableContents.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class ConsumableContents { - - String body; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableDecision.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableDecision.java deleted file mode 100644 index 7089e017b22..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableDecision.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class ConsumableDecision { - - ConsumablePricing pricing; - - @JsonProperty("adId") - Long adId; - - @JsonProperty("bidderName") - String bidderName; - - @JsonProperty("creativeId") - String creativeId; - - List contents; - - @JsonProperty("impressionUrl") - String impressionUrl; - - Integer width; - - Integer height; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumablePlacement.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumablePlacement.java deleted file mode 100644 index bc813d19793..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumablePlacement.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class ConsumablePlacement { - - @JsonProperty("divName") - String divName; - - @JsonProperty("networkId") - Integer networkId; - - @JsonProperty("siteId") - Integer siteId; - - @JsonProperty("unitId") - Integer unitId; - - @JsonProperty("unitName") - String unitName; - - @JsonProperty("adTypes") - List adTypes; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumablePricing.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumablePricing.java deleted file mode 100644 index 705594902d6..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumablePricing.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class ConsumablePricing { - - @JsonProperty("clearPrice") - Double clearPrice; -} diff --git a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableUser.java b/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableUser.java deleted file mode 100644 index d40fe2c8597..00000000000 --- a/src/main/java/org/prebid/server/bidder/consumable/model/ConsumableUser.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.consumable.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor -@Value -public class ConsumableUser { - - String key; -} diff --git a/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java b/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java new file mode 100644 index 00000000000..f8d739193a9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.copper6ssp; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.copper6ssp.proto.Copper6SspImpExtBidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.copper6ssp.ImpExtCopper6Ssp; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class Copper6SspBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public Copper6SspBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ImpExtCopper6Ssp extImp; + try { + extImp = parseImpExt(imp); + outgoingRequests.add(makeRequest(modifyImp(imp, extImp), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(outgoingRequests) + ? Result.withErrors(errors) + : Result.of(outgoingRequests, errors); + } + + private ImpExtCopper6Ssp parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ImpExtCopper6Ssp extImp) { + final Copper6SspImpExtBidder impExtBidder = getImpExtWithType(extImp); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(impExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private Copper6SspImpExtBidder getImpExtWithType(ImpExtCopper6Ssp impExtCopper6Ssp) { + final boolean hasPlacementId = StringUtils.isNotBlank(impExtCopper6Ssp.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(impExtCopper6Ssp.getEndpointId()); + + return Copper6SspImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? impExtCopper6Ssp.getPlacementId() : null) + .endpointId(hasEndpointId ? impExtCopper6Ssp.getEndpointId() : null) + .build(); + } + + private HttpRequest makeRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java b/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java new file mode 100644 index 00000000000..5feb52a144b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.copper6ssp.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class Copper6SspImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java new file mode 100644 index 00000000000..d6f43ef4c86 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java @@ -0,0 +1,27 @@ +package org.prebid.server.bidder.criteo; + +import com.iab.openrtb.response.SeatBid; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class CriteoBidResponse { + + String id; + + List seatbid; + + String bidid; + + String cur; + + String customdata; + + Integer nbr; + + CriteoExtBidResponse ext; + +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java index db9194ee215..064bac48c48 100644 --- a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; @@ -19,6 +19,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -30,6 +31,7 @@ public class CriteoBidder implements Bidder { + private static final String BIDDER_NAME = "criteo"; private final String endpointUrl; private final JacksonMapper mapper; @@ -44,16 +46,24 @@ public Result>> makeHttpRequests(BidRequest bidRequ } @Override + @Deprecated(forRemoval = true) public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + return Result.withError(BidderError.generic("Deprecated adapter method invoked")); + } + + @Override + public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBidsFromResponse(bidResponse)); + final CriteoBidResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), + CriteoBidResponse.class); + return CompositeBidderResponse.withBids(extractBids(bidResponse), extractFledge(bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBidsFromResponse(BidResponse bidResponse) { + private List extractBids(CriteoBidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -94,4 +104,22 @@ private ObjectNode makeExt(String networkName) { .meta(ExtBidPrebidMeta.builder().networkName(networkName).build()) .build()); } + + private static List extractFledge(CriteoBidResponse bidResponse) { + final List fledgeConfigs = Optional.ofNullable(bidResponse) + .map(CriteoBidResponse::getExt) + .map(CriteoExtBidResponse::getIgi) + .filter(CollectionUtils::isNotEmpty) + .orElse(Collections.emptyList()) + .stream() + .filter(igi -> CollectionUtils.isNotEmpty(igi.getIgs()) && igi.getIgs().getFirst() != null) + .map(igi -> FledgeAuctionConfig.builder() + .impId(igi.getImpId()) + .bidder(BIDDER_NAME) + .config(igi.getIgs().getFirst().getConfig()) + .build()) + .toList(); + + return CollectionUtils.isEmpty(fledgeConfigs) ? null : fledgeConfigs; + } } diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java new file mode 100644 index 00000000000..8f332b2f3bd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoExtBidResponse { + + List igi; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java new file mode 100644 index 00000000000..6bdac80ad2f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoIgiExtBidResponse { + + String impId; + + List igs; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java new file mode 100644 index 00000000000..b34d76e0646 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.criteo; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class CriteoIgsIgiExtBidResponse { + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java b/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java index 4a848edb85b..d85f782ba03 100644 --- a/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java +++ b/src/main/java/org/prebid/server/bidder/deepintent/DeepintentBidder.java @@ -78,7 +78,7 @@ private Banner buildImpBanner(Banner banner, String impId) { if (CollectionUtils.isEmpty(banner.getFormat())) { throw new PreBidException("At least one size is required, imp : " + impId); } - final Format format = bannerFormats.get(0); + final Format format = bannerFormats.getFirst(); return banner.toBuilder().w(format.getW()).h(format.getH()).build(); } diff --git a/src/main/java/org/prebid/server/bidder/definemedia/DefineMediaBidder.java b/src/main/java/org/prebid/server/bidder/definemedia/DefineMediaBidder.java new file mode 100644 index 00000000000..d27bbe87cf2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/definemedia/DefineMediaBidder.java @@ -0,0 +1,91 @@ +package org.prebid.server.bidder.definemedia; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class DefineMediaBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public DefineMediaBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + final List bidderBids = extractBids(bidResponse, errors); + return Result.of(bidderBids, errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBidderBid(Bid bid, String bidCurrency, List errors) { + final JsonNode prebidNode = ObjectUtil.getIfNotNull(bid.getExt(), node -> node.get("prebid")); + final JsonNode typeNode = ObjectUtil.getIfNotNull(prebidNode, node -> node.get("type")); + final BidType bidType; + try { + bidType = mapper.mapper().convertValue(typeNode, BidType.class); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badServerResponse("Failed to parse impression %s mediatype" + .formatted(bid.getImpid()))); + return null; + } + + if (bidType != BidType.xNative && bidType != BidType.banner) { + errors.add(BidderError.badServerResponse("Invalid mediatype: %s in the impression %s" + .formatted(bidType, bid.getImpid()))); + return null; + } + + return BidderBid.of(bid, bidType, bidCurrency); + } +} diff --git a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java new file mode 100644 index 00000000000..fe0e8ad6a03 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java @@ -0,0 +1,184 @@ +package org.prebid.server.bidder.displayio; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.displayio.DisplayioImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class DisplayioBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String BIDDER_CURRENCY = "USD"; + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String X_OPENRTB_VERSION = "2.5"; + + private final CurrencyConversionService currencyConversionService; + private final String endpointUrl; + private final JacksonMapper mapper; + + public DisplayioBidder(CurrencyConversionService currencyConversionService, + String endpointUrl, + JacksonMapper mapper) { + + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final DisplayioImpExt impExt = parseImpExt(imp); + + final BidRequest modifiedBidRequest = request.toBuilder() + .imp(Collections.singletonList(modifyImp(request, imp))) + .ext(modifyExtRequest(request, impExt)) + .build(); + + final String url = resolveEndpoint(impExt); + requests.add(BidderUtil.defaultRequest(modifiedBidRequest, makeHeaders(), url, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(requests) + ? Result.withErrors(errors) + : Result.of(requests, errors); + + } + + private DisplayioImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp) { + return imp.toBuilder() + .bidfloor(resolveBidFloor(bidRequest, imp)) + .bidfloorcur(BIDDER_CURRENCY) + .build(); + } + + private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); + } + + return bidFloor; + } + + private ExtRequest modifyExtRequest(BidRequest request, DisplayioImpExt impExt) { + final ExtRequest extRequest = request.getExt(); + final ExtRequest modifiedExtRequest = Optional.ofNullable(extRequest) + .map(ext -> { + final ExtRequest copy = ExtRequest.of(extRequest.getPrebid()); + copy.addProperties(extRequest.getProperties()); + return copy; + }).orElseGet(ExtRequest::empty); + + final DisplayioRequestExt requestExt = DisplayioRequestExt.of(impExt.getInventoryId(), impExt.getPlacementId()); + modifiedExtRequest.addProperty("displayio", mapper.mapper().valueToTree(requestExt)); + + return modifiedExtRequest; + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers().set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + } + + private String resolveEndpoint(DisplayioImpExt impExt) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getPublisherId()))); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null + || CollectionUtils.isEmpty(bidResponse.getSeatbid()) + || bidResponse.getSeatbid().size() > 1) { + + throw new PreBidException("Invalid SeatBids count"); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> toBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid toBidderBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid.getMtype()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Integer mType) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException("unsupported MType " + mType); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/displayio/DisplayioRequestExt.java b/src/main/java/org/prebid/server/bidder/displayio/DisplayioRequestExt.java new file mode 100644 index 00000000000..0fd94d79b79 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/displayio/DisplayioRequestExt.java @@ -0,0 +1,14 @@ +package org.prebid.server.bidder.displayio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class DisplayioRequestExt { + + @JsonProperty("inventoryId") + String inventoryId; + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java b/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java index 281b1a9cb5f..3df9ed64eda 100644 --- a/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java +++ b/src/main/java/org/prebid/server/bidder/dmx/DmxBidder.java @@ -76,7 +76,7 @@ public Result>> makeHttpRequests(BidRequest request String updatedPublisherId = null; String updatedSellerId = null; try { - final ExtImpDmx extImp = parseImpExt(imps.get(0)); + final ExtImpDmx extImp = parseImpExt(imps.getFirst()); final String extImpPublisherId = extImp.getPublisherId(); updatedPublisherId = StringUtils.isNotBlank(extImpPublisherId) ? extImpPublisherId : extImp.getMemberId(); updatedSellerId = extImp.getSellerId(); @@ -203,7 +203,7 @@ private static Banner resolveBanner(Banner banner) { final List format = banner != null ? banner.getFormat() : null; if ((height == null || width == null) && CollectionUtils.isNotEmpty(format)) { - final Format firstFormat = format.get(0); + final Format firstFormat = format.getFirst(); if (firstFormat != null) { return banner.toBuilder() .w(firstFormat.getW()) diff --git a/src/main/java/org/prebid/server/bidder/driftpixel/DriftpixelBidder.java b/src/main/java/org/prebid/server/bidder/driftpixel/DriftpixelBidder.java new file mode 100644 index 00000000000..01b651a9189 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/driftpixel/DriftpixelBidder.java @@ -0,0 +1,133 @@ +package org.prebid.server.bidder.driftpixel; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.driftpixel.DriftpixelImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class DriftpixelBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String HOST_MACRO = "{{Host}}"; + private static final String SOURCE_ID_MACRO = "{{SourceId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public DriftpixelBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final DriftpixelImpExt impExt = parseImpExt(imp); + final BidRequest modifiedBidRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + requests.add(BidderUtil.defaultRequest(modifiedBidRequest, resolveEndpoint(impExt), mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private DriftpixelImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private String resolveEndpoint(DriftpixelImpExt impExt) { + return endpointUrl + .replace(HOST_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getEnv()))) + .replace(SOURCE_ID_MACRO, HttpUtil.encodeUrl(impExt.getPid())); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Array SeatBid cannot be empty"); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + final BidType mediaType = getBidMediaType(bid); + return BidderBid.of(bid, mediaType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + } + + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "failed to parse bid mtype (%d) for impression id \"%s\"".formatted(markupType, bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java b/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java index c511ba6f371..0c3ce43b489 100644 --- a/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidder.java @@ -197,7 +197,7 @@ private static Banner modifyImpBanner(Banner banner) { final List formatSkipFirst = originalFormat.subList(1, originalFormat.size()); bannerBuilder.format(formatSkipFirst); - final Format firstFormat = originalFormat.get(0); + final Format firstFormat = originalFormat.getFirst(); bannerBuilder.w(firstFormat.getW()); bannerBuilder.h(firstFormat.getH()); diff --git a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java index 276aa0e39b7..25578dafb75 100644 --- a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java +++ b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java @@ -18,6 +18,7 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -39,6 +40,10 @@ public class EpsilonBidder implements Bidder { + private static final String BIDDER_CURRENCY = "USD"; + + private final CurrencyConversionService currencyConversionService; + private static final TypeReference> EPSILON_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -59,10 +64,14 @@ public class EpsilonBidder implements Bidder { private final boolean generateBidId; private final JacksonMapper mapper; - public EpsilonBidder(String endpointUrl, boolean generateBidId, JacksonMapper mapper) { + public EpsilonBidder(String endpointUrl, + boolean generateBidId, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.generateBidId = generateBidId; this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); } @Override @@ -84,22 +93,35 @@ private BidRequest createOutgoingRequest(BidRequest bidRequest) { for (int i = 0; i < requestImps.size(); i++) { final Imp imp = requestImps.get(i); final ExtImpEpsilon impExt = parseImpExt(imp, i); - modifiedImps.add(modifyImp(imp, impExt)); + final BigDecimal bidFloor = resolveBidFloor(bidRequest, + imp.getBidfloorcur(), + getBidFloor(imp.getBidfloor(), impExt.getBidfloor())); + modifiedImps.add(modifyImp(imp, impExt, bidFloor)); } - final Imp firstImp = requestImps.get(0); + final Imp firstImp = requestImps.getFirst(); final ExtImpEpsilon extImp = parseImpExt(firstImp, 0); final String siteId = extImp.getSiteId(); final Site requestSite = bidRequest.getSite(); final App requestApp = bidRequest.getApp(); - + final List cur = bidRequest.getCur(); return bidRequest.toBuilder() .site(updateSite(requestSite, siteId)) .app(requestSite == null ? updateApp(requestApp, siteId) : requestApp) .imp(modifiedImps) + .cur(Collections.singletonList(BIDDER_CURRENCY)) .build(); } + private BigDecimal resolveBidFloor(BidRequest bidRequest, String bidfloorcur, BigDecimal bidfloor) { + if (BidderUtil.isValidPrice(bidfloor) + && !StringUtils.equalsIgnoreCase(bidfloorcur, BIDDER_CURRENCY) + && StringUtils.isNotBlank(bidfloorcur)) { + return currencyConversionService.convertCurrency(bidfloor, bidRequest, bidfloorcur, BIDDER_CURRENCY); + } + return bidfloor; + } + private ExtImpEpsilon parseImpExt(Imp imp, int impIndex) { final ExtImpEpsilon extImp; try { @@ -122,14 +144,15 @@ private static App updateApp(App app, String siteId) { return app == null ? null : app.toBuilder().id(siteId).build(); } - private static Imp modifyImp(Imp imp, ExtImpEpsilon impExt) { + private static Imp modifyImp(Imp imp, ExtImpEpsilon impExt, BigDecimal bidfloor) { final Banner banner = imp.getBanner(); final Video video = imp.getVideo(); return imp.toBuilder() .displaymanager(DISPLAY_MANAGER) .displaymanagerver(DISPLAY_MANAGER_VER) - .bidfloor(getBidFloor(imp.getBidfloor(), impExt.getBidfloor())) + .bidfloor(bidfloor) + .bidfloorcur(BIDDER_CURRENCY) .tagid(getTagId(imp.getTagid(), impExt.getTagId())) .secure(getSecure(imp, impExt)) .banner(modifyBanner(banner, impExt.getPosition())) @@ -217,7 +240,7 @@ private List extractBids(BidderCall httpCall) { } private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); final List bids = firstSeatBid.getBid(); if (CollectionUtils.isEmpty(bids)) { diff --git a/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java b/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java new file mode 100644 index 00000000000..6d520a2ecd0 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java @@ -0,0 +1,140 @@ +package org.prebid.server.bidder.escalax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.escalax.ExtImpEscalax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class EscalaxBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String X_OPENRTB_VERSION = "2.5"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public EscalaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpEscalax extImp; + try { + extImp = parseImpExt(firstImp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + return Result.withValue(makeHttpRequest(createRequest(request), extImp)); + } + + private static BidRequest createRequest(BidRequest request) { + return request.toBuilder().imp(prepareFirstImp(request.getImp())).build(); + } + + private static List prepareFirstImp(List imps) { + final Imp firstImp = imps.getFirst(); + final List updatedImps = new ArrayList<>(imps); + updatedImps.set(0, firstImp.toBuilder().ext(null).build()); + + return updatedImps; + } + + private HttpRequest makeHttpRequest(BidRequest bidRequest, ExtImpEscalax extImp) { + return BidderUtil.defaultRequest(bidRequest, makeHeaders(bidRequest.getDevice()), makeUrl(extImp), mapper); + } + + private String makeUrl(ExtImpEscalax extImp) { + return endpointUrl + .replace("{{AccountID}}", extImp.getAccountId()) + .replace("{{SourceId}}", extImp.getSourceId()); + } + + private MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + headers.set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, + ObjectUtil.getIfNotNull(device, Device::getUa)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIpv6)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIp)); + + return headers; + } + + private ExtImpEscalax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing escalaxExt - " + e.getMessage()); + } + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unsupported MType " + mtype); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/evolution/EvolutionBidder.java b/src/main/java/org/prebid/server/bidder/evolution/EvolutionBidder.java index 378027e8e0f..9bc64b3c12b 100644 --- a/src/main/java/org/prebid/server/bidder/evolution/EvolutionBidder.java +++ b/src/main/java/org/prebid/server/bidder/evolution/EvolutionBidder.java @@ -57,7 +57,7 @@ private List extractBids(BidResponse bidResponse) { } private List bidsFromResponse(BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); return CollectionUtils.emptyIfNull(firstSeatBid.getBid()).stream() .filter(Objects::nonNull) .map(bid -> BidderBid.of(bid, getBidMediaType(bid.getExt()), bidResponse.getCur())) diff --git a/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java b/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java index aba3e2b2d1f..7fc42804347 100644 --- a/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java +++ b/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java @@ -144,7 +144,7 @@ private static PrebidRequest createPrebidRequest(Imp imp, ExtImpFlipp extImp) { final Format format = Optional.ofNullable(imp.getBanner()) .map(Banner::getFormat) .filter(CollectionUtils::isNotEmpty) - .map(formats -> formats.get(0)) + .map(formats -> formats.getFirst()) .orElse(null); return PrebidRequest.builder() @@ -297,7 +297,7 @@ private static boolean isInlineValid(BidRequest bidRequest, Inline inline) { private static Bid constructBid(Inline inline) { final Prebid prebid = inline.getPrebid(); final Data data = Optional.ofNullable(inline.getContents()) - .map(content -> content.get(0)) + .map(content -> content.getFirst()) .map(Content::getData) .orElse(null); diff --git a/src/main/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidder.java b/src/main/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidder.java index c8f41b7282d..868516f5efb 100644 --- a/src/main/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidder.java +++ b/src/main/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidder.java @@ -1,9 +1,12 @@ package org.prebid.server.bidder.freewheelssp; +import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.vertx.core.http.HttpMethod; +import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -11,12 +14,17 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.freewheelssp.ExtImpFreewheelSSP; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -24,9 +32,14 @@ public class FreewheelSSPBidder implements Bidder { - private static final BidType BID_TYPE = BidType.video; + private static final TypeReference> FREEWHEELSSP_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String COMPONENT_ID_HEADER_NAME = "Componentid"; private static final String COMPONENT_ID_HEADER_VALUE = "prebid-java"; + private static final BidType BID_TYPE = BidType.video; + private final String endpointUrl; private final JacksonMapper mapper; @@ -36,16 +49,48 @@ public FreewheelSSPBidder(String endpointUrl, JacksonMapper mapper) { } @Override - public final Result>> makeHttpRequests(BidRequest bidRequest) { + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List modifiedImps = new ArrayList<>(); + + try { + for (final Imp imp : bidRequest.getImp()) { + final ExtImpFreewheelSSP extImpFreewheelSSP = parseExtImp(imp); + modifiedImps.add(modifyImp(imp, extImpFreewheelSSP)); + } + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + return Result.withValue( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(HttpUtil.headers().add(COMPONENT_ID_HEADER_NAME, COMPONENT_ID_HEADER_VALUE)) - .body(mapper.encodeToBytes(bidRequest)) - .impIds(BidderUtil.impIds(bidRequest)) - .payload(bidRequest) - .build()); + BidderUtil.defaultRequest( + modifyBidRequest(bidRequest, modifiedImps), + headers(), + endpointUrl, + mapper)); + } + + private ExtImpFreewheelSSP parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), FREEWHEELSSP_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext for impression id %s. Error Infomation: %s" + .formatted(imp.getId(), e.getMessage())); + } + } + + private Imp modifyImp(Imp imp, ExtImpFreewheelSSP extImpFreewheelSSP) { + return imp.toBuilder() + .ext(mapper.mapper().valueToTree(extImpFreewheelSSP)) + .build(); + } + + private static BidRequest modifyBidRequest(BidRequest bidRequest, List imps) { + return bidRequest.toBuilder().imp(imps).build(); + } + + private static MultiMap headers() { + return HttpUtil.headers().add(COMPONENT_ID_HEADER_NAME, COMPONENT_ID_HEADER_VALUE); } @Override @@ -62,16 +107,31 @@ private static List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidResponse); - } - private static List bidsFromResponse(BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BID_TYPE, bidResponse.getCur())) + .map(bid -> BidderBid.builder() + .bid(bid) + .type(BID_TYPE) + .bidCurrency(bidResponse.getCur()) + .videoInfo(videoInfo(bid)) + .build()) .toList(); } + + private static ExtBidPrebidVideo videoInfo(Bid bid) { + final List cat = bid.getCat(); + final Integer duration = bid.getDur(); + + final boolean catNotEmpty = CollectionUtils.isNotEmpty(cat); + final boolean durationValid = duration != null && duration > 0; + return catNotEmpty || durationValid + ? ExtBidPrebidVideo.of( + durationValid ? duration : null, + catNotEmpty ? cat.getFirst() : null) + : null; + } } diff --git a/src/main/java/org/prebid/server/bidder/gamma/GammaBidder.java b/src/main/java/org/prebid/server/bidder/gamma/GammaBidder.java index 6a1e1fc7d3c..c92786904f0 100644 --- a/src/main/java/org/prebid/server/bidder/gamma/GammaBidder.java +++ b/src/main/java/org/prebid/server/bidder/gamma/GammaBidder.java @@ -98,7 +98,7 @@ private static Imp modifyImp(Imp imp) { if (banner != null) { final List format = banner.getFormat(); if (banner.getW() == null && banner.getH() == null && CollectionUtils.isNotEmpty(format)) { - final Format firstFormat = format.get(0); + final Format firstFormat = format.getFirst(); final Banner modifiedBanner = banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); return imp.toBuilder().banner(modifiedBanner).build(); } diff --git a/src/main/java/org/prebid/server/bidder/gamoshi/GamoshiBidder.java b/src/main/java/org/prebid/server/bidder/gamoshi/GamoshiBidder.java index 88ee576791e..6d85c4ff511 100644 --- a/src/main/java/org/prebid/server/bidder/gamoshi/GamoshiBidder.java +++ b/src/main/java/org/prebid/server/bidder/gamoshi/GamoshiBidder.java @@ -62,7 +62,7 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpGamoshi firstImpExt; try { - firstImpExt = parseAndValidateImpExt(validImps.get(0)); + firstImpExt = parseAndValidateImpExt(validImps.getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -87,7 +87,7 @@ private static Imp processImp(Imp imp) { final Banner banner = imp.getBanner(); if (banner != null && banner.getH() == null && banner.getW() == null && CollectionUtils.isNotEmpty(banner.getFormat())) { - final Format firstFormat = banner.getFormat().get(0); + final Format firstFormat = banner.getFormat().getFirst(); final Banner modifiedBanner = banner.toBuilder() .h(firstFormat.getH()) .w(firstFormat.getW()) diff --git a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java index fe2ac533dc4..e444038f329 100644 --- a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java @@ -48,7 +48,7 @@ public GothamAdsBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final GothamAdsImpExt impExt; - final Imp firstImp = request.getImp().get(0); + final Imp firstImp = request.getImp().getFirst(); try { impExt = parseImpExt(firstImp); } catch (PreBidException e) { @@ -77,7 +77,7 @@ private GothamAdsImpExt parseImpExt(Imp imp) { private static BidRequest cleanUpFirstImpExt(BidRequest request) { final List imps = new ArrayList<>(request.getImp()); - imps.set(0, request.getImp().get(0).toBuilder().ext(null).build()); + imps.set(0, request.getImp().getFirst().toBuilder().ext(null).build()); return request.toBuilder().imp(imps).build(); } diff --git a/src/main/java/org/prebid/server/bidder/grid/GridBidder.java b/src/main/java/org/prebid/server/bidder/grid/GridBidder.java index 8ecabeff5ab..6f5b0e93b35 100644 --- a/src/main/java/org/prebid/server/bidder/grid/GridBidder.java +++ b/src/main/java/org/prebid/server/bidder/grid/GridBidder.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; @@ -16,6 +17,7 @@ import org.prebid.server.bidder.grid.model.request.ExtImp; import org.prebid.server.bidder.grid.model.request.ExtImpGridData; import org.prebid.server.bidder.grid.model.request.ExtImpGridDataAdServer; +import org.prebid.server.bidder.grid.model.request.GridNative; import org.prebid.server.bidder.grid.model.request.Keywords; import org.prebid.server.bidder.grid.model.response.GridBidResponse; import org.prebid.server.bidder.grid.model.response.GridSeatBid; @@ -68,7 +70,7 @@ public Result>> makeHttpRequests(BidRequest request return Result.withErrors(errors); } - final Keywords firstImpKeywords = getKeywordsFromImpExt(imps.get(0).getExt()); + final Keywords firstImpKeywords = getKeywordsFromImpExt(imps.getFirst().getExt()); final BidRequest modifiedRequest = modifyRequest(request, firstImpKeywords, modifiedImps); return Result.of(Collections.singletonList( @@ -103,19 +105,48 @@ private static void validateImpExt(ExtImp extImp, String impId) { } private Imp modifyImp(Imp imp, ExtImp extImp) { + return imp.toBuilder() + .xNative(modifyNative(imp.getXNative())) + .ext(modifyImpExt(extImp)) + .build(); + } + + private ObjectNode modifyImpExt(ExtImp extImp) { final ExtImpGridData extImpData = extImp.getData(); final ExtImpGridDataAdServer adServer = extImpData != null ? extImpData.getAdServer() : null; final String adSlot = adServer != null ? adServer.getAdSlot() : null; if (StringUtils.isNotEmpty(adSlot)) { - final ExtImp modifiedExtImp = extImp.toBuilder() - .gpid(adSlot) - .build(); - return imp.toBuilder() - .ext(mapper.mapper().valueToTree(modifiedExtImp)) - .build(); + final ExtImp modifiedExtImp = extImp.toBuilder().gpid(adSlot).build(); + return mapper.mapper().valueToTree(modifiedExtImp); + } + + return mapper.mapper().valueToTree(extImp); + } + + private Native modifyNative(Native xNative) { + if (xNative == null) { + return null; + } + + final String nativeRequest = xNative.getRequest(); + final JsonNode requestNode = nodeFromString(nativeRequest); + + return GridNative.builder() + .requestNative((ObjectNode) requestNode) + .ver(xNative.getVer()) + .api(xNative.getApi()) + .battr(xNative.getBattr()) + .ext(xNative.getExt()) + .build(); + } + + public final JsonNode nodeFromString(String stringValue) { + try { + return StringUtils.isNotBlank(stringValue) ? mapper.mapper().readTree(stringValue) : null; + } catch (Exception e) { + return null; } - return imp; } private Keywords getKeywordsFromImpExt(JsonNode extImp) { @@ -218,7 +249,7 @@ private List bidsFromResponse(BidRequest bidRequest, GridBidResponse private BidderBid makeBidderBid(ObjectNode bidNode, List imps, String currency) { try { final Bid bid = mapper.mapper().treeToValue(bidNode, Bid.class); - final Bid modifiedBid = bid.toBuilder().ext(modifyBidExt(bidNode)).build(); + final Bid modifiedBid = bid.toBuilder().adm(modifyAdm(bidNode, bid)).ext(modifyBidExt(bidNode)).build(); return BidderBid.of(modifiedBid, resolveBidType(bidNode, bid.getImpid(), imps), currency); } catch (JsonProcessingException | IllegalArgumentException e) { @@ -226,6 +257,16 @@ private BidderBid makeBidderBid(ObjectNode bidNode, List imps, String curre } } + private String modifyAdm(ObjectNode bidNode, Bid bid) { + final JsonNode admNative = bidNode.at("/adm_native"); + final String bidAdm = bid.getAdm(); + if (admNative != null && !admNative.isEmpty() && StringUtils.isBlank(bidAdm)) { + return mapper.encodeToString(admNative); + } + + return bidAdm; + } + private ObjectNode modifyBidExt(ObjectNode gridBid) { final String demandSource = ObjectUtils.defaultIfNull(gridBid, MissingNode.getInstance()) .at("/ext/bidder/grid/demandSource") @@ -252,6 +293,8 @@ private static BidType resolveBidType(ObjectNode bidNode, String impId, List format = banner.getFormat(); if (banner.getH() == null && banner.getW() == null && CollectionUtils.isNotEmpty(format)) { - final Format firstFormat = format.get(0); + final Format firstFormat = format.getFirst(); final Long slot = extImpGumgum.getSlot(); final ObjectNode bannerExt = slot != null && slot != 0L @@ -174,18 +173,6 @@ private static ExtImpGumgumBanner resolveBannerExt(List formats, Long sl .orElseGet(() -> ExtImpGumgumBanner.of(slot, 0, 0)); } - private void validateVideoParams(Video video) { - if (anyOfNull( - video.getW(), - video.getH(), - video.getMinduration(), - video.getMaxduration(), - video.getPlacement(), - video.getLinearity())) { - throw new PreBidException("Invalid or missing video field(s)"); - } - } - private Video resolveVideo(Video video, String irisId) { final ObjectNode videoExt = mapper.mapper().valueToTree(ExtImpGumgumVideo.of(irisId)); return video.toBuilder().ext(videoExt).build(); diff --git a/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdmBuilder.java b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdmBuilder.java index c1d6e0d2002..5c4bb8f2235 100644 --- a/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdmBuilder.java +++ b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdmBuilder.java @@ -210,13 +210,13 @@ private String buildRewardedVideoPart(Content content, Integer adWidth, Integer final String contentId = content.getContentId(); final List iconList = metaData.getIconList(); - final Icon firstIcon = CollectionUtils.isNotEmpty(iconList) ? iconList.get(0) : null; + final Icon firstIcon = CollectionUtils.isNotEmpty(iconList) ? iconList.getFirst() : null; if (firstIcon != null && StringUtils.isNotBlank(firstIcon.getUrl())) { return buildIconRewardedPart(contentId, clickUrl, adWidth, adHeight, firstIcon); } final List imageInfoList = metaData.getImageInfoList(); - final ImageInfo firstImage = CollectionUtils.isNotEmpty(imageInfoList) ? imageInfoList.get(0) : null; + final ImageInfo firstImage = CollectionUtils.isNotEmpty(imageInfoList) ? imageInfoList.getFirst() : null; if (firstImage != null && StringUtils.isNotBlank(firstImage.getUrl())) { return buildImageRewardedPart(contentId, clickUrl, adWidth, adHeight, firstImage); } diff --git a/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiDeviceBuilder.java b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiDeviceBuilder.java index 4f7c5a1b781..ff95dcd40ea 100644 --- a/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiDeviceBuilder.java +++ b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiDeviceBuilder.java @@ -79,9 +79,9 @@ private Device makeDeviceWithDeviceId(com.iab.openrtb.request.Device device, Use final String gaid = isGaidEmpty ? deviceIfa.orElseThrow(() -> new PreBidException("getDeviceID: openRTBRequest.User.Ext is nil " + "and device.Gaid is not specified.")) - : userData.getGaid().get(0); - final String oaid = isOaidEmpty ? null : userData.getOaid().get(0); - final String imei = isImeiEmpty ? null : userData.getImei().get(0); + : userData.getGaid().getFirst(); + final String oaid = isOaidEmpty ? null : userData.getOaid().getFirst(); + final String imei = isImeiEmpty ? null : userData.getImei().getFirst(); final String clientTime = Optional.ofNullable(userData) .map(ExtUserDataDeviceIdHuaweiAds::getClientTime) .map(this::formatClientTime) @@ -104,7 +104,7 @@ private ExtUserDataDeviceIdHuaweiAds parseUserExtData(ExtUser extUser) { } private String formatClientTime(List clientTimes) { - return CollectionUtils.isEmpty(clientTimes) ? null : clientTimeFormatter.format(clientTimes.get(0)); + return CollectionUtils.isEmpty(clientTimes) ? null : clientTimeFormatter.format(clientTimes.getFirst()); } } diff --git a/src/main/java/org/prebid/server/bidder/huaweiads/model/response/MonitorEventType.java b/src/main/java/org/prebid/server/bidder/huaweiads/model/response/MonitorEventType.java index 0abd4455b4d..e74273a7046 100644 --- a/src/main/java/org/prebid/server/bidder/huaweiads/model/response/MonitorEventType.java +++ b/src/main/java/org/prebid/server/bidder/huaweiads/model/response/MonitorEventType.java @@ -3,8 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; @Getter @@ -33,19 +31,18 @@ public static MonitorEventType of(String monitorEvent) { private static final Map EVENT_TYPE_MAP = stringToEventTypeMap(); private static Map stringToEventTypeMap() { - final Map result = new HashMap<>(); - result.put("imp", IMP); - result.put("click", CLICK); - result.put("vastError", VAST_ERROR); - result.put("userclose", USER_CLOSE); - result.put("playStart", PLAY_START); - result.put("playEnd", PLAY_END); - result.put("playResume", PLAY_RESUME); - result.put("playPause", PLAY_PAUSE); - result.put("soundClickOff", SOUND_CLICK_OFF); - result.put("soundClickOn", SOUND_CLICK_ON); - result.put("win", WIN); - return Collections.unmodifiableMap(result); + return Map.ofEntries( + Map.entry("imp", IMP), + Map.entry("click", CLICK), + Map.entry("vastError", VAST_ERROR), + Map.entry("userclose", USER_CLOSE), + Map.entry("playStart", PLAY_START), + Map.entry("playEnd", PLAY_END), + Map.entry("playResume", PLAY_RESUME), + Map.entry("playPause", PLAY_PAUSE), + Map.entry("soundClickOff", SOUND_CLICK_OFF), + Map.entry("soundClickOn", SOUND_CLICK_ON), + Map.entry("win", WIN)); } } diff --git a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java index 99b56d59f8a..e86a6182eb9 100644 --- a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java @@ -4,11 +4,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -26,13 +24,10 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.improvedigital.ExtImpImprovedigital; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; import java.util.Collection; @@ -50,9 +45,6 @@ public class ImprovedigitalBidder implements Bidder { private static final TypeReference> IMPROVEDIGITAL_EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String CONSENT_PROVIDERS_SETTINGS_OUT_KEY = "consented_providers_settings"; - private static final String CONSENTED_PROVIDERS_KEY = "consented_providers"; - private static final String REGEX_SPLIT_STRING_BY_DOT = "\\."; private static final String IS_REWARDED_INVENTORY_FIELD = "is_rewarded_inventory"; private static final JsonPointer IS_REWARDED_INVENTORY_POINTER @@ -89,46 +81,6 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValues(httpRequests); } - private ExtUser getAdditionalConsentProvidersUserExt(ExtUser extUser) { - final String consentedProviders = ObjectUtil.getIfNotNull( - ObjectUtil.getIfNotNull(extUser, ExtUser::getConsentedProvidersSettings), - ConsentedProvidersSettings::getConsentedProviders); - - if (StringUtils.isBlank(consentedProviders)) { - return extUser; - } - - final String[] consentedProvidersParts = StringUtils.split(consentedProviders, "~"); - final String consentedProvidersPart = consentedProvidersParts.length > 1 ? consentedProvidersParts[1] : null; - if (StringUtils.isBlank(consentedProvidersPart)) { - return extUser; - } - - return fillExtUser(extUser, consentedProvidersPart.split(REGEX_SPLIT_STRING_BY_DOT)); - } - - private ExtUser fillExtUser(ExtUser extUser, String[] arrayOfSplitString) { - final JsonNode consentProviderSettingJsonNode; - try { - consentProviderSettingJsonNode = customJsonNode(arrayOfSplitString); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - - return mapper.fillExtension(extUser, consentProviderSettingJsonNode); - } - - private JsonNode customJsonNode(String[] arrayOfSplitString) { - final Integer[] integers = mapper.mapper().convertValue(arrayOfSplitString, Integer[].class); - final ArrayNode arrayNode = mapper.mapper().createArrayNode(); - for (Integer integer : integers) { - arrayNode.add(integer); - } - - return mapper.mapper().createObjectNode().set(CONSENT_PROVIDERS_SETTINGS_OUT_KEY, - mapper.mapper().createObjectNode().set(CONSENTED_PROVIDERS_KEY, arrayNode)); - } - private ExtImpImprovedigital parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), IMPROVEDIGITAL_EXT_TYPE_REFERENCE).getBidder(); @@ -149,12 +101,8 @@ private static Imp updateImp(Imp imp) { } private HttpRequest resolveRequest(BidRequest bidRequest, Imp imp, Integer publisherId) { - final User user = bidRequest.getUser(); final BidRequest modifiedRequest = bidRequest.toBuilder() .imp(Collections.singletonList(updateImp(imp))) - .user(user != null - ? user.toBuilder().ext(getAdditionalConsentProvidersUserExt(user.getExt())).build() - : null) .build(); final String pathPrefix = publisherId != null && publisherId > 0 @@ -250,7 +198,7 @@ private static BidType getBidType(Bid bid) { case 2 -> BidType.video; case 3 -> BidType.audio; case 4 -> BidType.xNative; - default -> throw new PreBidException( + case null, default -> throw new PreBidException( "Unsupported mtype %d for impression with ID: \"%s\"".formatted(bid.getMtype(), bid.getImpid())); }; } diff --git a/src/main/java/org/prebid/server/bidder/infytv/InfytvBidder.java b/src/main/java/org/prebid/server/bidder/infytv/InfytvBidder.java deleted file mode 100644 index 237c7b133fc..00000000000 --- a/src/main/java/org/prebid/server/bidder/infytv/InfytvBidder.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.prebid.server.bidder.infytv; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -public class InfytvBidder implements Bidder { - - private final String endpointUrl; - private final JacksonMapper mapper; - - public InfytvBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest bidRequest) { - return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); - } - - @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse)); - } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse("Bad Response, " + e.getMessage())); - } catch (PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private static List extractBids(BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - throw new PreBidException("Empty SeatBid array"); - } - return bidsFromResponse(bidResponse); - } - - private static List bidsFromResponse(BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, BidType.video, bidResponse.getCur())) - .toList(); - } -} diff --git a/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java b/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java index 62e8c8df7a9..33e8d25e9d7 100644 --- a/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; @@ -49,7 +50,7 @@ public InmobiBidder(String endpointUrl, JacksonMapper mapper) { public Result>> makeHttpRequests(BidRequest request) { final List errors = new ArrayList<>(); - final Imp imp = request.getImp().get(FIRST_IMP_INDEX); + final Imp imp = request.getImp().getFirst(); final ExtImpInmobi extImpInmobi; try { @@ -84,7 +85,7 @@ private Imp updateImp(Imp imp) { if (banner != null) { if ((banner.getW() == null || banner.getH() == null || banner.getW() == 0 || banner.getH() == 0) && CollectionUtils.isNotEmpty(banner.getFormat())) { - final Format format = banner.getFormat().get(0); + final Format format = banner.getFormat().getFirst(); return imp.toBuilder().banner(banner.toBuilder().w(format.getW()).h(format.getH()).build()).build(); } } @@ -95,40 +96,37 @@ private Imp updateImp(Imp imp) { public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.of(extractBids(bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); + return bidsFromResponse(bidResponse); } - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private List bidsFromResponse(BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) .toList(); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - } - } - return BidType.banner; + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unsupported mtype %d for bid %s" + .formatted(mtype, bid.getId())); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidder.java b/src/main/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidder.java index c52bef8888b..adb7aae4a04 100644 --- a/src/main/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidder.java +++ b/src/main/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidder.java @@ -36,7 +36,7 @@ public InteractiveOffersBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final ObjectNode impExt = request.getImp().get(0).getExt(); + final ObjectNode impExt = request.getImp().getFirst().getExt(); final String resolvedPartnerId = StringUtils.defaultString(resolvePartnerId(impExt)); final String resolvedEndpointUrl = endpointUrl.replace("{{PartnerId}}", resolvedPartnerId); diff --git a/src/main/java/org/prebid/server/bidder/intertech/IntertechBidder.java b/src/main/java/org/prebid/server/bidder/intertech/IntertechBidder.java index 686a865dfbb..8f6b2afd4ea 100644 --- a/src/main/java/org/prebid/server/bidder/intertech/IntertechBidder.java +++ b/src/main/java/org/prebid/server/bidder/intertech/IntertechBidder.java @@ -16,8 +16,8 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; @@ -83,7 +83,7 @@ private String getReferer(BidRequest request) { private String getCur(BidRequest request) { final List curs = request.getCur(); - return curs != null && !curs.isEmpty() ? curs.get(0) : ""; + return curs != null && !curs.isEmpty() ? curs.getFirst() : ""; } private ExtImpIntertech parseAndValidateImpExt(ObjectNode impExtNode, final String impId) { @@ -124,7 +124,7 @@ private static Banner updateBanner(Banner banner) { final List format = banner.getFormat(); if (w == null || h == null || w == 0 || h == 0) { if (CollectionUtils.isNotEmpty(format)) { - final Format firstFormat = format.get(0); + final Format firstFormat = format.getFirst(); return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); } throw new PreBidException("Invalid sizes provided for Banner %sx%s".formatted(w, h)); diff --git a/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java b/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java index 7785cd476b8..c6f473bc52a 100644 --- a/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java +++ b/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; @@ -49,32 +50,43 @@ public Result>> makeHttpRequests(BidRequest request final List> httpRequests = new ArrayList<>(); for (Imp imp : request.getImp()) { + final ExtImpIqzone extImpIqzone; try { - final ExtImpIqzone extImpIqzone = parseImpExt(imp); - final Imp modifiedImp = modifyImp(imp, extImpIqzone); - - httpRequests.add(makeHttpRequest(request, modifiedImp)); + extImpIqzone = parseImpExt(imp); } catch (IllegalArgumentException e) { return Result.withError(BidderError.badInput(e.getMessage())); } + + final Imp modifiedImp = modifyImp(imp, extImpIqzone); + httpRequests.add(makeHttpRequest(request, modifiedImp)); } return Result.withValues(httpRequests); } private ExtImpIqzone parseImpExt(Imp imp) { - return mapper.mapper().convertValue(imp.getExt(), IQZONE_EXT_TYPE_REFERENCE).getBidder(); + try { + return mapper.mapper().convertValue(imp.getExt(), IQZONE_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } } private Imp modifyImp(Imp imp, ExtImpIqzone impExt) { final String placementId = impExt.getPlacementId(); - final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + final String endpointId = impExt.getEndpointId(); + + final boolean isPlacementIdEmpty = StringUtils.isEmpty(placementId); + if (isPlacementIdEmpty && StringUtils.isEmpty(endpointId)) { + return imp; + } - if (StringUtils.isNotEmpty(placementId)) { + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + if (!isPlacementIdEmpty) { modifiedImpExtBidder.set("placementId", TextNode.valueOf(placementId)); modifiedImpExtBidder.set("type", TextNode.valueOf("publisher")); } else { - modifiedImpExtBidder.set("endpointId", TextNode.valueOf(impExt.getEndpointId())); + modifiedImpExtBidder.set("endpointId", TextNode.valueOf(endpointId)); modifiedImpExtBidder.set("type", TextNode.valueOf("network")); } @@ -84,8 +96,7 @@ private Imp modifyImp(Imp imp, ExtImpIqzone impExt) { } private HttpRequest makeHttpRequest(BidRequest request, Imp imp) { - final BidRequest outgoingRequest = request.toBuilder().imp(List.of(imp)).build(); - + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); } @@ -93,45 +104,39 @@ private HttpRequest makeHttpRequest(BidRequest request, Imp imp) { public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(bidResponse)); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); - } - - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidMediaType(bid), bidResponse.getCur())) .toList(); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getBanner() != null) { - return BidType.banner; - } - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - throw new PreBidException("Unknown impression type for ID: \"%s\"".formatted(impId)); - } + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); } - throw new PreBidException("Failed to find impression for ID: \"%s\"".formatted(impId)); + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid()); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java index 9843eedca9e..5c7c468af8f 100644 --- a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java @@ -41,7 +41,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.proto.openrtb.ext.request.ix.ExtImpIx; import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; @@ -107,21 +106,6 @@ public Result>> makeHttpRequests(BidRequest bidRequ return Result.of(httpRequests, errors); } - @Override - public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { - try { - final IxBidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), IxBidResponse.class); - final List bidderErrors = new ArrayList<>(); - return CompositeBidderResponse.builder() - .bids(extractIxBids(bidRequest, bidResponse, bidderErrors)) - .fledgeAuctionConfigs(extractFledge(bidResponse)) - .errors(bidderErrors) - .build(); - } catch (DecodeException e) { - return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); - } - } - private ExtImpIx parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), IX_EXT_TYPE_REFERENCE).getBidder(); @@ -166,7 +150,7 @@ private UpdateResult modifyImpBanner(Banner banner) { final Banner modifiedBanner = banner.toBuilder().format(newFormats).build(); return UpdateResult.updated(modifiedBanner); } else if (formats.size() == 1) { - final Format format = formats.get(0); + final Format format = formats.getFirst(); final Banner modifiedBanner = banner.toBuilder().w(format.getW()).h(format.getH()).build(); return UpdateResult.updated(modifiedBanner); } @@ -175,11 +159,7 @@ private UpdateResult modifyImpBanner(Banner banner) { } private BidRequest modifyBidRequest(BidRequest bidRequest, List imps, Set siteIds) { - final String publisherId = Optional.of(siteIds) - .filter(siteIdsSet -> siteIdsSet.size() == 1) - .map(Collection::stream) - .flatMap(Stream::findFirst) - .orElse(null); + final String publisherId = siteIds.size() == 1 ? siteIds.stream().findAny().get() : null; return bidRequest.toBuilder() .imp(imps) @@ -189,12 +169,36 @@ private BidRequest modifyBidRequest(BidRequest bidRequest, List imps, Set builder.publisher(modifyPublisher(site.getPublisher(), id))) + .map(Site.SiteBuilder::build) + .orElse(null); + } + + private static App modifyApp(App app, String id) { + return Optional.ofNullable(app) + .map(App::toBuilder) + .map(builder -> builder.publisher(modifyPublisher(app.getPublisher(), id))) + .map(App.AppBuilder::build) + .orElse(null); + } + + private static Publisher modifyPublisher(Publisher publisher, String id) { + return Optional.ofNullable(publisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .id(id) + .build(); + } + private ExtRequest modifyRequestExt(ExtRequest extRequest, Set siteIds) { final ExtRequest modifiedExt; if (extRequest != null) { modifiedExt = ExtRequest.of(extRequest.getPrebid()); - modifiedExt.addProperties(extRequest.getProperties()); + mapper.fillExtension(modifiedExt, extRequest); } else { modifiedExt = ExtRequest.empty(); } @@ -219,44 +223,41 @@ private IxDiag makeDiagData(ExtRequest extRequest, Set siteIds) { return IxDiag.of(pbsv, pbjsv, multipleSiteIds); } - private static Site modifySite(Site site, String id) { - return Optional.ofNullable(site) - .map(Site::toBuilder) - .map(builder -> builder.publisher(modifyPublisher(site.getPublisher(), id))) - .map(Site.SiteBuilder::build) - .orElse(null); - } - - private static App modifyApp(App app, String id) { - return Optional.ofNullable(app) - .map(App::toBuilder) - .map(builder -> builder.publisher(modifyPublisher(app.getPublisher(), id))) - .map(App.AppBuilder::build) - .orElse(null); - } - - private static Publisher modifyPublisher(Publisher publisher, String id) { - return Optional.ofNullable(publisher) - .map(Publisher::toBuilder) - .orElseGet(Publisher::builder) - .id(id) - .build(); - } - @Override @Deprecated(since = "Not used, since Bidder.makeBidderResponse(...) was overridden.") public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { return Result.withError(BidderError.generic("Invalid method call")); } - private List bidsFromResponse(IxBidResponse bidResponse, - BidRequest bidRequest, - List errors) { + @Override + public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { + try { + final IxBidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), IxBidResponse.class); + final List bidderErrors = new ArrayList<>(); + return CompositeBidderResponse.builder() + .bids(extractBids(bidRequest, bidResponse, bidderErrors)) + .fledgeAuctionConfigs(extractFledge(bidResponse)) + .errors(bidderErrors) + .build(); + } catch (DecodeException e) { + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidRequest bidRequest, + IxBidResponse bidResponse, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) + .filter(Objects::nonNull) .map(bid -> toBidderBid(bid, bidRequest, bidResponse, errors)) .filter(Objects::nonNull) .toList(); @@ -271,68 +272,21 @@ private BidderBid toBidderBid(Bid bid, BidRequest bidRequest, IxBidResponse bidR return null; } + final ExtBidPrebidVideo extBidPrebidVideo = bidType == BidType.video + ? parseBidExtPrebidVideo(bid.getExt()) + : null; + final Bid updatedBid = switch (bidType) { - case video -> updateBidWithVideoAttributes(bid); - case xNative -> bid.toBuilder().adm(updateBidAdmWithNativeAttributes(bid.getAdm())).build(); + case video -> updateBidWithVideoAttributes(bid, extBidPrebidVideo); + case xNative -> updateBidAdmWithNativeAttributes(bid); default -> bid; }; - return BidderBid.of(updatedBid, bidType, bidResponse.getCur()); - } - - private Bid updateBidWithVideoAttributes(Bid bid) { - final ObjectNode bidExt = bid.getExt(); - final ExtBidPrebid extPrebid = bidExt != null ? parseBidExt(bidExt) : null; - final ExtBidPrebidVideo extVideo = extPrebid != null ? extPrebid.getVideo() : null; - final Bid updatedBid; - if (extVideo != null) { - final Bid.BidBuilder bidBuilder = bid.toBuilder(); - bidBuilder.ext(resolveBidExt(extVideo.getDuration())); - if (CollectionUtils.isEmpty(bid.getCat())) { - bidBuilder.cat(Collections.singletonList(extVideo.getPrimaryCategory())).build(); - } - updatedBid = bidBuilder.build(); - } else { - updatedBid = bid; - } - return updatedBid; - } - - private String updateBidAdmWithNativeAttributes(String adm) { - final NativeV11Wrapper nativeV11 = parseBidAdm(adm, NativeV11Wrapper.class); - final Response responseV11 = ObjectUtil.getIfNotNull(nativeV11, NativeV11Wrapper::getNativeResponse); - final boolean isV11 = responseV11 != null; - final Response response = isV11 ? responseV11 : parseBidAdm(adm, Response.class); - final List trackers = ObjectUtil.getIfNotNull(response, Response::getEventtrackers); - final String updatedAdm = CollectionUtils.isNotEmpty(trackers) ? mapper.encodeToString(isV11 - ? NativeV11Wrapper.of(mergeNativeImpTrackers(response, trackers)) - : mergeNativeImpTrackers(response, trackers)) - : null; - - return updatedAdm != null ? updatedAdm : adm; - } - - private T parseBidAdm(String adm, Class clazz) { - try { - return mapper.decodeValue(adm, clazz); - } catch (IllegalArgumentException | DecodeException e) { - return null; - } - } - - private static Response mergeNativeImpTrackers(Response response, List eventTrackers) { - final List impressionAndImageTrackers = eventTrackers.stream() - .filter(tracker -> Objects.equals(tracker.getMethod(), EventType.IMPRESSION.getValue()) - || Objects.equals(tracker.getEvent(), EventTrackingMethod.IMAGE.getValue())) - .toList(); - final List impTrackers = Stream.concat( - impressionAndImageTrackers.stream().map(EventTracker::getUrl), - response.getImptrackers().stream()) - .distinct() - .toList(); - - return response.toBuilder() - .imptrackers(impTrackers) + return BidderBid.builder() + .bid(updatedBid) + .type(bidType) + .bidCurrency(bidResponse.getCur()) + .videoInfo(bidType == BidType.video ? videoInfo(extBidPrebidVideo) : null) .build(); } @@ -343,13 +297,13 @@ private static BidType getBidType(Bid bid, List imps) { } private static Optional getBidTypeFromMtype(Integer mType) { - final BidType bidType = mType != null ? switch (mType) { + final BidType bidType = switch (mType) { case 1 -> BidType.banner; case 2 -> BidType.video; case 3 -> BidType.audio; case 4 -> BidType.xNative; - default -> null; - } : null; + case null, default -> null; + }; return Optional.ofNullable(bidType); } @@ -379,26 +333,77 @@ private static BidType getBidTypeFromImp(List imps, String impId) { throw new PreBidException("Unmatched impression id " + impId); } - private ExtBidPrebid parseBidExt(ObjectNode bidExt) { + private ExtBidPrebidVideo parseBidExtPrebidVideo(ObjectNode bidExt) { + if (bidExt == null) { + return null; + } + try { - return mapper.mapper().treeToValue(bidExt, ExtBidPrebid.class); + return mapper.mapper().treeToValue(bidExt.path("prebid").path("video"), ExtBidPrebidVideo.class); } catch (JsonProcessingException e) { return null; } } - private ObjectNode resolveBidExt(Integer duration) { - return mapper.mapper().valueToTree(ExtBidPrebid.builder() - .video(ExtBidPrebidVideo.of(duration, null)) - .build()); + private Bid updateBidWithVideoAttributes(Bid bid, ExtBidPrebidVideo extBidPrebidVideo) { + return CollectionUtils.isEmpty(bid.getCat()) && extBidPrebidVideo != null + ? bid.toBuilder() + .cat(Collections.singletonList(extBidPrebidVideo.getPrimaryCategory())) + .build() + : bid; + } + + private Bid updateBidAdmWithNativeAttributes(Bid bid) { + final String adm = bid.getAdm(); + final NativeV11Wrapper nativeV11 = parseBidAdm(adm, NativeV11Wrapper.class); + final Response responseV11 = ObjectUtil.getIfNotNull(nativeV11, NativeV11Wrapper::getNativeResponse); + final boolean isV11 = responseV11 != null; + final Response response = isV11 ? responseV11 : parseBidAdm(adm, Response.class); + final List trackers = ObjectUtil.getIfNotNull(response, Response::getEventtrackers); + final String updatedAdm = CollectionUtils.isNotEmpty(trackers) + ? mergeNativeImpTrackers(isV11, response, trackers) + : null; + + return updatedAdm != null + ? bid.toBuilder().adm(updatedAdm).build() + : bid; + } + + private T parseBidAdm(String adm, Class clazz) { + try { + return mapper.decodeValue(adm, clazz); + } catch (IllegalArgumentException | DecodeException e) { + return null; + } + } + + private String mergeNativeImpTrackers(boolean isV11, Response response, List trackers) { + return mapper.encodeToString(isV11 + ? NativeV11Wrapper.of(mergeNativeImpTrackers(response, trackers)) + : mergeNativeImpTrackers(response, trackers)); } - private List extractIxBids(BidRequest bidRequest, - IxBidResponse bidResponse, - List bidderErrors) { - return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) - ? Collections.emptyList() - : bidsFromResponse(bidResponse, bidRequest, bidderErrors); + private static Response mergeNativeImpTrackers(Response response, List eventTrackers) { + return response.toBuilder() + .imptrackers(Stream.concat( + eventTrackers.stream() + .filter(IxBidder::isImpTracker) + .map(EventTracker::getUrl), + response.getImptrackers().stream()) + .distinct() + .toList()) + .build(); + } + + private static boolean isImpTracker(EventTracker tracker) { + return Objects.equals(tracker.getMethod(), EventType.IMPRESSION.getValue()) + || Objects.equals(tracker.getEvent(), EventTrackingMethod.IMAGE.getValue()); + } + + private static ExtBidPrebidVideo videoInfo(ExtBidPrebidVideo extBidPrebidVideo) { + return extBidPrebidVideo != null + ? ExtBidPrebidVideo.of(extBidPrebidVideo.getDuration(), null) + : null; } private List extractFledge(IxBidResponse bidResponse) { diff --git a/src/main/java/org/prebid/server/bidder/kayzen/KayzenBidder.java b/src/main/java/org/prebid/server/bidder/kayzen/KayzenBidder.java index d20a592ac73..342c64dee21 100644 --- a/src/main/java/org/prebid/server/bidder/kayzen/KayzenBidder.java +++ b/src/main/java/org/prebid/server/bidder/kayzen/KayzenBidder.java @@ -47,7 +47,7 @@ public KayzenBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List originalImps = request.getImp(); - final Imp firstImp = originalImps.get(FIRST_IMP_INDEX); + final Imp firstImp = originalImps.getFirst(); final ExtImpKayzen extImpKayzen; try { diff --git a/src/main/java/org/prebid/server/bidder/krushmedia/KrushmediaBidder.java b/src/main/java/org/prebid/server/bidder/krushmedia/KrushmediaBidder.java index c4b63560247..835d3781a1a 100644 --- a/src/main/java/org/prebid/server/bidder/krushmedia/KrushmediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/krushmedia/KrushmediaBidder.java @@ -53,7 +53,7 @@ public Result>> makeHttpRequests(BidRequest request final String url; try { - extImpKrushmedia = parseImpExt(request.getImp().get(0)); + extImpKrushmedia = parseImpExt(request.getImp().getFirst()); url = resolveEndpoint(extImpKrushmedia.getAccountId()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -127,7 +127,7 @@ private List extractBids(BidRequest bidRequest, BidResponse bidRespon } private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); return firstSeatBid.getBid().stream() .filter(Objects::nonNull) diff --git a/src/main/java/org/prebid/server/bidder/lemmadigital/LemmaDigitalBidder.java b/src/main/java/org/prebid/server/bidder/lemmadigital/LemmaDigitalBidder.java index 200ae89580f..beae5890d09 100644 --- a/src/main/java/org/prebid/server/bidder/lemmadigital/LemmaDigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/lemmadigital/LemmaDigitalBidder.java @@ -49,7 +49,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ return Result.withError(BidderError.badInput("Impression array should not be empty")); } - final Imp imp = bidRequest.getImp().get(0); + final Imp imp = bidRequest.getImp().getFirst(); final ExtImpLemmaDigital extImpLemmaDigital; try { @@ -116,6 +116,6 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon } private static BidType resolveBidType(BidRequest bidRequest) { - return bidRequest.getImp().get(0).getVideo() != null ? BidType.video : BidType.banner; + return bidRequest.getImp().getFirst().getVideo() != null ? BidType.video : BidType.banner; } } diff --git a/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java b/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java deleted file mode 100644 index bba94b55f9c..00000000000 --- a/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java +++ /dev/null @@ -1,174 +0,0 @@ -package org.prebid.server.bidder.liftoff; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.User; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.liftoff.model.LiftoffImpressionExt; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Price; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class LiftoffBidder implements Bidder { - - private static final String BIDDER_CURRENCY = "USD"; - private static final String X_OPENRTB_VERSION = "2.5"; - - private final String endpointUrl; - private final CurrencyConversionService currencyConversionService; - private final JacksonMapper mapper; - - public LiftoffBidder(String endpointUrl, - CurrencyConversionService currencyConversionService, - JacksonMapper mapper) { - - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.currencyConversionService = Objects.requireNonNull(currencyConversionService); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest bidRequest) { - final List errors = new ArrayList<>(); - final List> httpRequests = new ArrayList<>(); - - for (Imp imp : bidRequest.getImp()) { - try { - final Price price = resolveBidFloor(imp, bidRequest); - final LiftoffImpressionExt impExt = parseImpExt(imp); - final LiftoffImpressionExt modifiedImpExt = modifyImpExt(impExt, bidRequest); - final Imp modifiedImp = modifyImp(imp, modifiedImpExt, price); - final BidRequest modifiedRequest = modifyBidRequest( - bidRequest, - modifiedImp, - modifiedImpExt.getBidder().getAppStoreId()); - - httpRequests.add(makeHttpRequest(modifiedRequest)); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - } - - return Result.of(httpRequests, errors); - } - - private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { - BigDecimal bigDecimal = null; - if (BidderUtil.isValidPrice(imp.getBidfloor()) - && !StringUtils.equalsIgnoreCase(imp.getBidfloorcur(), BIDDER_CURRENCY) - && StringUtils.isNotBlank(imp.getBidfloorcur())) { - bigDecimal = currencyConversionService.convertCurrency( - imp.getBidfloor(), bidRequest, imp.getBidfloorcur(), BIDDER_CURRENCY); - } - - return Price.of(BIDDER_CURRENCY, bigDecimal); - } - - private LiftoffImpressionExt parseImpExt(Imp imp) { - return mapper.mapper().convertValue(imp.getExt(), LiftoffImpressionExt.class); - } - - private static LiftoffImpressionExt modifyImpExt(LiftoffImpressionExt impExt, BidRequest bidRequest) { - final ExtImpLiftoff bidder = impExt.getBidder(); - final String buyerId = ObjectUtil.getIfNotNull(bidRequest.getUser(), User::getBuyeruid); - final ExtImpLiftoff vungle = ExtImpLiftoff.of( - buyerId, - bidder.getAppStoreId(), - bidder.getPlacementReferenceId()); - - return impExt.toBuilder().vungle(vungle).build(); - } - - private Imp modifyImp(Imp imp, LiftoffImpressionExt modifiedImpExt, Price price) { - return imp.toBuilder() - .tagid(modifiedImpExt.getBidder().getPlacementReferenceId()) - .ext(mapper.mapper().convertValue(modifiedImpExt, ObjectNode.class)) - .bidfloor(price.getValue() != null ? price.getValue() : imp.getBidfloor()) - .bidfloorcur(price.getValue() != null ? price.getCurrency() : imp.getBidfloorcur()) - .build(); - } - - private static BidRequest modifyBidRequest(BidRequest bidRequest, Imp imp, String appStoreId) { - final App app = bidRequest.getApp(); - final Site site = bidRequest.getSite(); - if (app == null && site == null) { - throw new PreBidException("The bid request must have an app or site object"); - } - return bidRequest.toBuilder() - .imp(Collections.singletonList(imp)) - .app(app == null ? App.builder().id(appStoreId).build() : app.toBuilder().id(appStoreId).build()) - .site(null) - .build(); - } - - private HttpRequest makeHttpRequest(BidRequest request) { - return HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .impIds(BidderUtil.impIds(request)) - .headers(headers()) - .payload(request) - .body(mapper.encodeToBytes(request)) - .build(); - } - - private static MultiMap headers() { - return HttpUtil.headers() - .add(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); - } - - @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse)); - } catch (DecodeException | PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private static List extractBids(BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - return bidsFromResponse(bidResponse); - } - - private static List bidsFromResponse(BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.video, bidResponse.getCur())) - .toList(); - } -} diff --git a/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java b/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java deleted file mode 100644 index 541867ad71a..00000000000 --- a/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.bidder.liftoff.model; - -import lombok.Builder; -import lombok.Getter; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; - -@Builder(toBuilder = true) -@Getter -public class LiftoffImpressionExt { - - ExtImpPrebid prebid; - - ExtImpLiftoff bidder; - - ExtImpLiftoff vungle; -} diff --git a/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java b/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java index 20161acc420..3774ef5e060 100644 --- a/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java +++ b/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java @@ -4,6 +4,7 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -22,10 +23,12 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; public class LoopmeBidder implements Bidder { private final String endpointUrl; + private final JacksonMapper mapper; public LoopmeBidder(String endpointUrl, JacksonMapper mapper) { @@ -36,7 +39,14 @@ public LoopmeBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - return Result.withValue(BidderUtil.defaultRequest(request, endpointUrl, mapper)); + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(request)) + .body(mapper.encodeToBytes(request)) + .payload(request) + .build()); } @Override @@ -63,7 +73,7 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon .filter(Objects::nonNull) .flatMap(Collection::stream) .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) - .toList(); + .collect(Collectors.toList()); } private static BidType getBidType(String impId, List imps) { @@ -73,6 +83,8 @@ private static BidType getBidType(String impId, List imps) { return BidType.banner; } else if (imp.getVideo() != null) { return BidType.video; + } else if (imp.getAudio() != null) { + return BidType.audio; } else if (imp.getXNative() != null) { return BidType.xNative; } diff --git a/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java b/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java new file mode 100644 index 00000000000..b5da6c1a0d2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java @@ -0,0 +1,162 @@ +package org.prebid.server.bidder.loyal; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.loyal.proto.LoyalImpExt; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.loyal.ExtImpLoyal; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class LoyalBidder implements Bidder { + + private static final TypeReference> LOYAL_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PUBLISHER_PROPERTY = "publisher"; + private static final String NETWORK_PROPERTY = "network"; + private static final String BIDDER_PROPERTY = "bidder"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public LoyalBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpLoyal extImpLoyal = parseExtImp(imp); + final Imp modifiedImp = modifyImp(imp, extImpLoyal); + httpRequests.add(makeHttpRequest(request, modifiedImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (httpRequests.isEmpty()) { + return Result.withError(BidderError.badInput("found no valid impressions")); + } + + return Result.of(httpRequests, errors); + } + + private ExtImpLoyal parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), LOYAL_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Cannot deserialize ExtImpLoyal: " + e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpLoyal extImpLoyal) { + final LoyalImpExt impExtLoyalWithType = resolveImpExt(extImpLoyal); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + modifiedImpExtBidder.set(BIDDER_PROPERTY, mapper.mapper().valueToTree(impExtLoyalWithType)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private LoyalImpExt resolveImpExt(ExtImpLoyal extImpLoyal) { + final LoyalImpExt.LoyalImpExtBuilder builder = LoyalImpExt.builder(); + + if (StringUtils.isNotEmpty(extImpLoyal.getPlacementId())) { + builder.type(PUBLISHER_PROPERTY).placementId(extImpLoyal.getPlacementId()); + } else if (StringUtils.isNotEmpty(extImpLoyal.getEndpointId())) { + builder.type(NETWORK_PROPERTY).endpointId(extImpLoyal.getEndpointId()); + } + + return builder.build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, Imp imp) { + final BidRequest outgoingRequest = request.toBuilder().imp(List.of(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List bids = extractBids(bidResponse); + return Result.withValues(bids); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private BidType getBidType(Bid bid) { + final JsonNode typeNode = Optional.ofNullable(bid.getExt()) + .map(extNode -> extNode.get("prebid")) + .map(extPrebidNode -> extPrebidNode.get("type")) + .orElse(null); + + final BidType bidType; + try { + bidType = mapper.mapper().convertValue(typeNode, BidType.class); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to parse bid.ext.prebid.type for bid.id: '%s'" + .formatted(bid.getId())); + } + + if (bidType == null) { + throw new PreBidException("bid.ext.prebid.type is not present for bid.id: '%s'" + .formatted(bid.getId())); + } + + return switch (bidType) { + case banner, video, xNative -> bidType; + default -> throw new PreBidException("Unsupported BidType: " + + bidType.getName() + " for bid.id: '" + bid.getId() + "'"); + }; + } + +} diff --git a/src/main/java/org/prebid/server/bidder/loyal/proto/LoyalImpExt.java b/src/main/java/org/prebid/server/bidder/loyal/proto/LoyalImpExt.java new file mode 100644 index 00000000000..10c8c8d55f5 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/loyal/proto/LoyalImpExt.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.loyal.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class LoyalImpExt { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/lunamedia/LunamediaBidder.java b/src/main/java/org/prebid/server/bidder/lunamedia/LunamediaBidder.java index 728ac621f39..a0ef293d3d0 100644 --- a/src/main/java/org/prebid/server/bidder/lunamedia/LunamediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/lunamedia/LunamediaBidder.java @@ -130,7 +130,7 @@ private static Banner modifyImpBanner(Banner banner) { final List formatSkipFirst = originalFormat.subList(1, originalFormat.size()); bannerBuilder.format(formatSkipFirst); - final Format firstFormat = originalFormat.get(0); + final Format firstFormat = originalFormat.getFirst(); bannerBuilder.w(firstFormat.getW()); bannerBuilder.h(firstFormat.getH()); diff --git a/src/main/java/org/prebid/server/bidder/marsmedia/MarsmediaBidder.java b/src/main/java/org/prebid/server/bidder/marsmedia/MarsmediaBidder.java index 07277179d63..ea7e4f9b4f2 100644 --- a/src/main/java/org/prebid/server/bidder/marsmedia/MarsmediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/marsmedia/MarsmediaBidder.java @@ -51,7 +51,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final String firstImpZone; final BidRequest outgoingRequest; try { - firstImpZone = resolveExtZone(bidRequest.getImp().get(0)); + firstImpZone = resolveExtZone(bidRequest.getImp().getFirst()); outgoingRequest = createRequest(bidRequest); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -110,7 +110,7 @@ private static BidRequest createRequest(BidRequest request) { } private static Banner updateBanner(Banner banner) { - final Format firstFormat = banner.getFormat().get(0); + final Format firstFormat = banner.getFormat().getFirst(); return banner.toBuilder() .w(ObjectUtils.defaultIfNull(firstFormat.getW(), 0)) .h(ObjectUtils.defaultIfNull(firstFormat.getH(), 0)) @@ -147,7 +147,7 @@ private static List extractBids(BidResponse bidResponse, BidRequest b } private static List bidsFromResponse(List seatbid, List imps, String currency) { - final SeatBid firstSeatBid = seatbid.get(0); + final SeatBid firstSeatBid = seatbid.getFirst(); return firstSeatBid != null ? firstSeatBid.getBid().stream() .filter(Objects::nonNull) .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), imps), currency)) diff --git a/src/main/java/org/prebid/server/bidder/mediago/MediaGoBidder.java b/src/main/java/org/prebid/server/bidder/mediago/MediaGoBidder.java new file mode 100644 index 00000000000..69cc2cc52a5 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediago/MediaGoBidder.java @@ -0,0 +1,219 @@ +package org.prebid.server.bidder.mediago; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.mediago.MediaGoImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class MediaGoBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String BIDDER_NAME = "mediago"; + private static final String HOST_MACRO = "{{Host}}"; + private static final String ACCOUNT_ID_MACRO = "{{AccountID}}"; + private static final String X_OPENRTB_VERSION = "2.5"; + private static final String DEFAULT_REGION = "us"; + + private static final Map REGIONS_MAP = Map.of( + "APAC", "jp", + "EU", "eu", + "US", "us"); + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MediaGoBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final MediaGoExt extRequest; + try { + extRequest = parseExt(request); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final List modifiedImps = new ArrayList<>(); + for (Imp imp : request.getImp()) { + final Imp modifiedImp = imp.toBuilder().banner(modifyBanner(imp.getBanner())).build(); + modifiedImps.add(modifiedImp); + } + + final BidRequest modifiedBidRequest = request.toBuilder().imp(modifiedImps).build(); + final String modifiedEndpoint = resolveEndpoint(extRequest); + return Result.withValue(BidderUtil.defaultRequest(modifiedBidRequest, makeHeaders(), modifiedEndpoint, mapper)); + } + + private MediaGoExt parseExt(BidRequest request) { + final MediaGoExt ext = parseExt(request.getExt()); + + if (ext != null && StringUtils.isNotBlank(ext.getToken())) { + return ext; + } + + final Imp firstImp = request.getImp().getFirst(); + final MediaGoImpExt impExt = parseImpExt(firstImp); + + if (StringUtils.isNotBlank(impExt.getToken())) { + return MediaGoExt.of(impExt.getToken(), impExt.getRegion()); + } + + throw new PreBidException("mediago token not found"); + } + + private MediaGoExt parseExt(ExtRequest ext) { + try { + return Optional.ofNullable(ext) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidderparams) + .map(bidders -> bidders.get(BIDDER_NAME)) + .map(bidderParams -> mapper.mapper().convertValue(bidderParams, MediaGoExt.class)) + .orElse(null); + } catch (IllegalArgumentException e) { + return null; + } + } + + private MediaGoImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Banner modifyBanner(Banner banner) { + if (banner == null) { + return null; + } + + final Integer width = banner.getW(); + final Integer height = banner.getH(); + final List formats = banner.getFormat(); + if ((width == null || width == 0 || height == null || height == 0) && CollectionUtils.isNotEmpty(formats)) { + final Format firstFormat = formats.getFirst(); + return banner.toBuilder() + .w(firstFormat.getW()) + .h(firstFormat.getH()) + .build(); + } + + return banner; + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers().set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + } + + private String resolveEndpoint(MediaGoExt ext) { + return endpointUrl + .replace(ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(ext.getToken())) + .replace(HOST_MACRO, HttpUtil.encodeUrl( + REGIONS_MAP.getOrDefault(StringUtils.defaultString(ext.getRegion()), DEFAULT_REGION))); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidRequest bidRequest, BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidRequest, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + + } + + private BidderBid makeBid(Bid bid, BidRequest bidRequest, String currency, List errors) { + final BidType bidType; + try { + bidType = getBidType(bid, bidRequest.getImp()); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, currency); + } + + private static BidType getBidType(Bid bid, List imps) { + return getBidTypeFromMtype(bid.getMtype()) + .or(() -> getBidTypeFromImp(imps, bid.getImpid())) + .orElseThrow(() -> new PreBidException("Unsupported MType " + bid.getMtype())); + } + + private static Optional getBidTypeFromMtype(Integer mType) { + final BidType bidType = switch (mType) { + case 1 -> BidType.banner; + case 4 -> BidType.xNative; + case null, default -> null; + }; + + return Optional.ofNullable(bidType); + } + + private static Optional getBidTypeFromImp(List imps, String impId) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return Optional.of(BidType.banner); + } else if (imp.getXNative() != null) { + return Optional.of(BidType.xNative); + } + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/mediago/MediaGoExt.java b/src/main/java/org/prebid/server/bidder/mediago/MediaGoExt.java new file mode 100644 index 00000000000..6a7576c0226 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediago/MediaGoExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.mediago; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediaGoExt { + + String token; + + String region; + +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java b/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java index 9f6181bcdf8..d184aefc297 100644 --- a/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java +++ b/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java @@ -2,25 +2,33 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.medianet.model.response.InterestGroupAuctionIntent; +import org.prebid.server.bidder.medianet.model.response.MedianetBidResponse; +import org.prebid.server.bidder.medianet.model.response.MedianetBidResponseExt; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; public class MedianetBidder implements Bidder { @@ -37,17 +45,37 @@ public Result>> makeHttpRequests(BidRequest bidRequ return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); } + /** + * @deprecated for this bidder in favor of @link{makeBidderResponse} which supports additional response data + */ @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + @Deprecated(forRemoval = true) + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + return Result.withError(BidderError.generic("Deprecated adapter method invoked")); + } + + @Override + public final CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { + final MedianetBidResponse bidResponse; try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), MedianetBidResponse.class); } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } + + final List errors = new ArrayList<>(); + final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); + final List fledgeAuctionConfigs = extractFledge(bidResponse); + + return CompositeBidderResponse.builder() + .bids(bids) + .fledgeAuctionConfigs(fledgeAuctionConfigs) + .errors(errors) + .build(); } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private static List extractBids(BidRequest bidRequest, MedianetBidResponse bidResponse, + List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -59,11 +87,40 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), currency)) + .map(bid -> makeBidderBid(bid, bidRequest.getImp(), currency, errors)) + .filter(Objects::nonNull) .toList(); } - private static BidType resolveBidType(String impId, List imps) { + private static BidType resolveBidType(Bid bid, List imps) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + return resolveBidTypeFromImpId(bid.getImpid(), imps); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType: %s" + .formatted(bid.getImpid())); + }; + } + + private static BidderBid makeBidderBid(Bid bid, List imps, String cur, List errors) { + final BidType bidType; + try { + bidType = resolveBidType(bid, imps); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, cur); + } + + private static BidType resolveBidTypeFromImpId(String impId, List imps) { for (Imp imp : imps) { if (Objects.equals(impId, imp.getId())) { if (imp.getBanner() != null) { @@ -80,4 +137,16 @@ private static BidType resolveBidType(String impId, List imps) { return BidType.banner; } + + private static List extractFledge(MedianetBidResponse bidResponse) { + return Optional.ofNullable(bidResponse) + .map(MedianetBidResponse::getExt) + .map(MedianetBidResponseExt::getIgi) + .orElse(Collections.emptyList()) + .stream() + .map(InterestGroupAuctionIntent::getIgs) + .flatMap(Collection::stream) + .map(e -> FledgeAuctionConfig.builder().impId(e.getImpId()).config(e.getConfig()).build()) + .toList(); + } } diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionBuyer.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionBuyer.java new file mode 100644 index 00000000000..04340fd8321 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionBuyer.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidder.medianet.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value +public class InterestGroupAuctionBuyer { + + String origin; + + @JsonProperty("maxbid") + Double maxBid; + + @JsonProperty("cur") + String currency; + + @JsonProperty("pbs") + String buyerSignals; + + @JsonProperty("ps") + ObjectNode prioritySignals; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionIntent.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionIntent.java new file mode 100644 index 00000000000..e060c1fb5dd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionIntent.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.medianet.model.response; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class InterestGroupAuctionIntent { + + List igb; + + List igs; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionSeller.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionSeller.java new file mode 100644 index 00000000000..15ae3ade13b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionSeller.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.medianet.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class InterestGroupAuctionSeller { + + @JsonProperty(value = "impid") + String impId; + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponse.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponse.java new file mode 100644 index 00000000000..4677294e6fc --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponse.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.medianet.model.response; + +import com.iab.openrtb.response.SeatBid; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +public class MedianetBidResponse { + + String id; + + List seatbid; + + String bidid; + + String cur; + + String customdata; + + Integer nbr; + + MedianetBidResponseExt ext; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponseExt.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponseExt.java new file mode 100644 index 00000000000..2ce5775704c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponseExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.medianet.model.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class MedianetBidResponseExt { + + List igi; +} diff --git a/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java b/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java new file mode 100644 index 00000000000..226a9a60149 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java @@ -0,0 +1,211 @@ +package org.prebid.server.bidder.melozen; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.melozen.MeloZenImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MeloZenBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String BIDDER_CURRENCY = "USD"; + private static final String EXT_PREBID = "prebid"; + + private final CurrencyConversionService currencyConversionService; + private final String endpointUrl; + private final JacksonMapper mapper; + + public MeloZenBidder(CurrencyConversionService currencyConversionService, + String endpoint, + JacksonMapper mapper) { + + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final MeloZenImpExt impExt = parseImpExt(imp); + final String url = resolveEndpoint(impExt); + final Imp modifiedImp = modifyImp(request, imp); + splitImpByMediaType(modifiedImp).forEach(splitImp -> + requests.add(BidderUtil.defaultRequest(modifyRequest(request, splitImp), url, mapper))); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private MeloZenImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp) { + final Price resolvedFloor = resolveBidFloor(bidRequest, imp); + return imp.toBuilder() + .bidfloor(resolvedFloor.getValue()) + .bidfloorcur(resolvedFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + bidFloor, + bidRequest, + bidFloorCurrency, + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedFloor); + } + + return Price.of(bidFloorCurrency, bidFloor); + } + + private String resolveEndpoint(MeloZenImpExt impExt) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getPubId()))); + } + + private List splitImpByMediaType(Imp imp) { + final Banner banner = imp.getBanner(); + final Video video = imp.getVideo(); + final Native xNative = imp.getXNative(); + + if (ObjectUtils.allNull(banner, video, xNative)) { + throw new PreBidException("Invalid MediaType. MeloZen only supports Banner, Video and Native."); + } + + final List imps = new ArrayList<>(); + + if (banner != null) { + imps.add(imp.toBuilder().video(null).xNative(null).build()); + } + + if (video != null) { + imps.add(imp.toBuilder().banner(null).xNative(null).build()); + } + + if (xNative != null) { + imps.add(imp.toBuilder().banner(null).video(null).build()); + } + + return imps; + } + + private BidRequest modifyRequest(BidRequest request, Imp imp) { + return request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> toBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid toBidderBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(() -> new PreBidException( + "Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid()))); + } + + private ExtBidPrebid parseExtBidPrebid(ObjectNode prebid) { + try { + return mapper.mapper().treeToValue(prebid, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java new file mode 100644 index 00000000000..08b7aec984e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java @@ -0,0 +1,158 @@ +package org.prebid.server.bidder.metax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.metax.ExtImpMetax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MetaxBidder implements Bidder { + + private static final TypeReference> METAX_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{publisherId}}"; + private static final String AD_UNIT_MACRO = "{{adUnit}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MetaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMetax extImpMetax = parseImpExt(imp); + httpRequests.add(BidderUtil.defaultRequest(prepareBidRequest(request, imp), + resolveEndpoint(extImpMetax), + mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpMetax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), METAX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static BidRequest prepareBidRequest(BidRequest bidRequest, Imp imp) { + return bidRequest.toBuilder() + .imp(Collections.singletonList(modifyImp(imp))) + .build(); + } + + private static Imp modifyImp(Imp imp) { + final Banner banner = imp.getBanner(); + final Integer width = banner != null ? banner.getW() : null; + final Integer height = banner != null ? banner.getH() : null; + if (width != null && height != null) { + return imp; + } + + final List formats = banner != null ? banner.getFormat() : null; + if (CollectionUtils.isEmpty(formats)) { + return imp; + } + + final Format firstFormat = formats.getFirst(); + return imp.toBuilder() + .banner(banner.toBuilder() + .w(Optional.ofNullable(firstFormat).map(Format::getW).orElse(0)) + .h(Optional.ofNullable(firstFormat).map(Format::getH).orElse(0)) + .build()) + .build(); + } + + private String resolveEndpoint(ExtImpMetax extImpMetax) { + final String publisherIdAsString = Optional.ofNullable(extImpMetax.getPublisherId()) + .map(Object::toString) + .orElse(StringUtils.EMPTY); + final String adUnitAsString = Optional.ofNullable(extImpMetax.getAdUnit()) + .map(Object::toString) + .orElse(StringUtils.EMPTY); + + return endpointUrl + .replace(PUBLISHER_ID_MACRO, publisherIdAsString) + .replace(AD_UNIT_MACRO, adUnitAsString); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unsupported MType: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/mgidx/MgidxBidder.java b/src/main/java/org/prebid/server/bidder/mgidx/MgidxBidder.java index f8c4ee9a068..bb01db86928 100644 --- a/src/main/java/org/prebid/server/bidder/mgidx/MgidxBidder.java +++ b/src/main/java/org/prebid/server/bidder/mgidx/MgidxBidder.java @@ -42,7 +42,6 @@ public class MgidxBidder implements Bidder { private static final String PUBLISHER_PROPERTY = "publisher"; private static final String NETWORK_PROPERTY = "network"; private static final String BIDDER_PROPERTY = "bidder"; - private static final String PREBID_EXT = "prebid"; private final String endpointUrl; private final JacksonMapper mapper; diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java new file mode 100644 index 00000000000..80bcba81fc6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -0,0 +1,25 @@ +package org.prebid.server.bidder.missena; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class MissenaAdRequest { + + String requestId; + + int timeout; + + String referer; + + String refererCanonical; + + String consentString; + + boolean consentRequired; + + String placement; + + String test; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java new file mode 100644 index 00000000000..6a34c31efbf --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java @@ -0,0 +1,21 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Builder +@Value +public class MissenaAdResponse { + + String ad; + + BigDecimal cpm; + + String currency; + + @JsonProperty("requestId") + String requestId; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java new file mode 100644 index 00000000000..913fd3b44b8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -0,0 +1,157 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MissenaBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final int AD_REQUEST_DEFAULT_TIMEOUT = 2000; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MissenaBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMissena extImp = parseImpExt(imp); + requests.add(makeHttpRequest(request, imp.getId(), extImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private ExtImpMissena parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing missenaExt parameters"); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, String impId, ExtImpMissena extImp) { + final Site site = request.getSite(); + + final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() + .requestId(request.getId()) + .timeout(AD_REQUEST_DEFAULT_TIMEOUT) + .referer(site == null ? null : site.getPage()) + .refererCanonical(site == null ? null : site.getDomain()) + .consentString(getUserConsent(request.getUser())) + .consentRequired(isGdpr(request.getRegs())) + .placement(extImp.getPlacement()) + .test(extImp.getTestMode()) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImp.getApiKey())) + .headers(makeHeaders(request.getDevice(), site)) + .impIds(Collections.singleton(impId)) + .body(mapper.encodeToBytes(missenaAdRequest)) + .payload(missenaAdRequest) + .build(); + } + + private MultiMap makeHeaders(Device device, Site site) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + } + + return headers; + } + + private String makeUrl(String apiKey) { + return endpointUrl + "?t=%s".formatted(apiKey); + } + + private static boolean isGdpr(Regs regs) { + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .map(gdpr -> gdpr == 1) + .orElse(false); + } + + private static String getUserConsent(User user) { + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(StringUtils.EMPTY); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final MissenaAdResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), + MissenaAdResponse.class); + return Result.withValues(Collections.singletonList(extractBid(bidRequest, bidResponse))); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private BidderBid extractBid(BidRequest request, MissenaAdResponse response) { + final Bid bid = Bid.builder() + .id(request.getId()) + .price(response.getCpm()) + .impid(request.getImp().getFirst().getId()) + .adm(response.getAd()) + .crid(response.getRequestId()) + .build(); + + return BidderBid.of(bid, BidType.banner, response.getCurrency()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidder.java b/src/main/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidder.java index 54cd4d19b67..9a322b2ae0d 100644 --- a/src/main/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidder.java +++ b/src/main/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidder.java @@ -55,7 +55,7 @@ public final Result>> makeHttpRequests(BidRequest b final BidRequest outgoingRequest; final String uri; try { - final Imp firstImp = bidRequest.getImp().get(0); + final Imp firstImp = bidRequest.getImp().getFirst(); final ExtImpMobfoxpb impExt = parseImpExt(firstImp); uri = buildUri(impExt.getKey()); diff --git a/src/main/java/org/prebid/server/bidder/model/BidderError.java b/src/main/java/org/prebid/server/bidder/model/BidderError.java index bf6fa169f7e..0873570a6d3 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderError.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderError.java @@ -94,7 +94,6 @@ public enum Type { * Covers the case where a bid was rejected by price-floors feature functionality */ rejected_ipf(6), - invalid_creative(350), timeout(1), generic(999); diff --git a/src/main/java/org/prebid/server/bidder/model/Price.java b/src/main/java/org/prebid/server/bidder/model/Price.java index c2e55939791..7a09e7f17fc 100644 --- a/src/main/java/org/prebid/server/bidder/model/Price.java +++ b/src/main/java/org/prebid/server/bidder/model/Price.java @@ -7,7 +7,13 @@ @Value(staticConstructor = "of") public class Price { + private static final Price EMPTY = Price.of(null, null); + String currency; BigDecimal value; + + public static Price empty() { + return Price.EMPTY; + } } diff --git a/src/main/java/org/prebid/server/bidder/motorik/MotorikBidder.java b/src/main/java/org/prebid/server/bidder/motorik/MotorikBidder.java index aafd9c38ec9..3fa086747b4 100644 --- a/src/main/java/org/prebid/server/bidder/motorik/MotorikBidder.java +++ b/src/main/java/org/prebid/server/bidder/motorik/MotorikBidder.java @@ -50,7 +50,7 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpMotorik firstImpExt; try { - firstImpExt = parseImpExt(request.getImp().get(0)); + firstImpExt = parseImpExt(request.getImp().getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -71,7 +71,7 @@ private static BidRequest createRequest(BidRequest request) { } private static List prepareFirstImp(List imps) { - final Imp firstImp = imps.get(0); + final Imp firstImp = imps.getFirst(); final List updatedImps = new ArrayList<>(imps); updatedImps.set(0, firstImp.toBuilder().ext(null).build()); diff --git a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java index 955ae3bb9d6..84c3f68e8bb 100644 --- a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java @@ -8,6 +8,7 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; @@ -21,6 +22,7 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; @@ -36,6 +38,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; public class NextMillenniumBidder implements Bidder { @@ -57,47 +60,53 @@ public NextMillenniumBidder(String endpointUrl, JacksonMapper mapper, List>> makeHttpRequests(BidRequest bidRequest) { final List errors = new ArrayList<>(); - final List impExts = getImpExts(bidRequest, errors); - - return errors.isEmpty() - ? Result.withValues(makeRequests(bidRequest, impExts)) - : Result.withErrors(errors); - } - - private List getImpExts(BidRequest bidRequest, List errors) { - return bidRequest.getImp().stream() - .map(imp -> convertExt(imp, errors)) - .toList(); - } + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + final ExtImpNextMillennium extImpNextMillennium; + try { + extImpNextMillennium = convertExt(imp.getExt()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + httpRequests.add(makeHttpRequest(updateBidRequest(bidRequest, extImpNextMillennium))); + } - private List> makeRequests(BidRequest bidRequest, List extImps) { - return extImps.stream() - .map(extImp -> makeHttpRequest(updateBidRequest(bidRequest, extImp))) - .toList(); + return errors.isEmpty() ? Result.withValues(httpRequests) : Result.withErrors(errors); } - private ExtImpNextMillennium convertExt(Imp imp, List errors) { + private ExtImpNextMillennium convertExt(ObjectNode impExt) { try { - return mapper.mapper() - .convertValue(imp.getExt(), NEXTMILLENNIUM_EXT_TYPE_REFERENCE) - .getBidder(); + return mapper.mapper().convertValue(impExt, NEXTMILLENNIUM_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - errors.add(BidderError.badInput(e.getMessage())); + throw new PreBidException(e.getMessage()); } - return null; } private BidRequest updateBidRequest(BidRequest bidRequest, ExtImpNextMillennium ext) { - final ExtRequestPrebid prebid = ExtRequestPrebid.builder() - .storedrequest(ExtStoredRequest.of(resolveStoredRequestId(bidRequest, ext))) + final ExtStoredRequest storedRequest = ExtStoredRequest.of(resolveStoredRequestId(bidRequest, ext)); + + final ExtRequestPrebid createdExtRequestPrebid = ExtRequestPrebid.builder() + .storedrequest(storedRequest) .build(); - final ExtRequest extRequest = ExtRequest.of(prebid); - final List imps = bidRequest.getImp().stream() - .map(imp -> imp.toBuilder().ext(createImpExt(prebid)).build()) - .toList(); + final ExtRequestPrebid extRequestPrebid = Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(prebid -> prebid.toBuilder().storedrequest(storedRequest).build()) + .orElse(createdExtRequestPrebid); + + return bidRequest.toBuilder() + .imp(updateImps(bidRequest, createdExtRequestPrebid)) + .ext(ExtRequest.of(extRequestPrebid)) + .build(); + } - return bidRequest.toBuilder().imp(imps).ext(extRequest).build(); + private List updateImps(BidRequest bidRequest, ExtRequestPrebid extRequestPrebid) { + return bidRequest.getImp().stream() + .map(imp -> imp.toBuilder().ext(createImpExt(extRequestPrebid)).build()) + .toList(); } private static String resolveStoredRequestId(BidRequest bidRequest, ExtImpNextMillennium extImpNextMillennium) { @@ -106,7 +115,7 @@ private static String resolveStoredRequestId(BidRequest bidRequest, ExtImpNextMi return extImpNextMillennium.getPlacementId(); } - final String size = formattedSizeFromBanner(bidRequest.getImp().get(0).getBanner()); + final String size = formattedSizeFromBanner(bidRequest.getImp().getFirst().getBanner()); final String domain = ObjectUtils.firstNonNull( ObjectUtil.getIfNotNull(bidRequest.getSite(), Site::getDomain), ObjectUtil.getIfNotNull(bidRequest.getApp(), App::getDomain), @@ -121,7 +130,7 @@ private static String formattedSizeFromBanner(Banner banner) { } final List formats = banner.getFormat(); - final Format firstFormat = CollectionUtils.isNotEmpty(formats) ? formats.get(0) : null; + final Format firstFormat = CollectionUtils.isNotEmpty(formats) ? formats.getFirst() : null; return ObjectUtils.firstNonNull( formatSize( @@ -164,24 +173,53 @@ private static MultiMap headers() { @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List bidderErrors = new ArrayList<>(); try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Result.empty(); } - return Result.withValues(bidsFromResponse(bidResponse)); + return Result.of(bidsFromResponse(bidResponse, bidderErrors), bidderErrors); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List bidsFromResponse(BidResponse bidResponse) { + private static List bidsFromResponse(BidResponse bidResponse, List bidderErrors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> resolveBidderBid(bidResponse, bidderErrors, bid)) + .filter(Objects::nonNull) .toList(); } + + private static BidderBid resolveBidderBid(BidResponse bidResponse, List bidderErrors, Bid bid) { + final BidType bidType = getBidType(bid, bidderErrors); + if (bidType == null) { + return null; + } + + return BidderBid.of(bid, bidType, bidResponse.getCur()); + } + + private static BidType getBidType(Bid bid, List bidderErrors) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + bidderErrors.add(BidderError.badServerResponse("Missing MType for bid: " + bid.getId())); + return null; + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + default -> { + bidderErrors.add(BidderError.badServerResponse( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid())); + yield null; + } + }; + } } diff --git a/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java b/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java index 94e9e47af80..9dfc6d158e3 100644 --- a/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java +++ b/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java @@ -3,7 +3,6 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -34,35 +33,27 @@ public OmsBidder(String endpointUrl, JacksonMapper mapper) { @Override public final Result>> makeHttpRequests(BidRequest bidRequest) { - return Result.withValue( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(HttpUtil.headers()) - .body(mapper.encodeToBytes(bidRequest)) - .impIds(BidderUtil.impIds(bidRequest)) - .payload(bidRequest) - .build()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); } @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + return Result.withValues(extractBids(bidResponse)); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private static List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); + return bidsFromResponse(bidResponse); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private static List bidsFromResponse(BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) diff --git a/src/main/java/org/prebid/server/bidder/openweb/OpenWebBidder.java b/src/main/java/org/prebid/server/bidder/openweb/OpenWebBidder.java index c95505a0af6..6d61491b8c3 100644 --- a/src/main/java/org/prebid/server/bidder/openweb/OpenWebBidder.java +++ b/src/main/java/org/prebid/server/bidder/openweb/OpenWebBidder.java @@ -1,13 +1,13 @@ package org.prebid.server.bidder.openweb; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -23,13 +23,10 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; public class OpenWebBidder implements Bidder { @@ -38,132 +35,110 @@ public class OpenWebBidder implements Bidder { new TypeReference<>() { }; - private final JacksonMapper mapper; private final String endpointUrl; + private final JacksonMapper mapper; public OpenWebBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = Objects.requireNonNull(HttpUtil.validateUrl(endpointUrl)); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { - final Map> sourceIdToModifiedImp = new HashMap<>(); - final List errors = new ArrayList<>(); + String org = null; for (Imp imp : request.getImp()) { try { final ExtImpOpenweb extImpOpenweb = parseImpExt(imp); - final Integer sourceId = extImpOpenweb.getSourceId(); - final Imp modifiedImp = modifyImp(imp, extImpOpenweb); + validateImpExt(extImpOpenweb); - if (sourceIdToModifiedImp.containsKey(sourceId)) { - sourceIdToModifiedImp.get(sourceId).add(modifiedImp); - } else { - sourceIdToModifiedImp.put(sourceId, new ArrayList<>(Collections.singletonList(modifiedImp))); + org = orgFrom(extImpOpenweb); + if (org != null) { + break; } } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput("checkExtAndExtractOrg: " + e.getMessage())); } } - if (sourceIdToModifiedImp.isEmpty()) { - return Result.withErrors(errors); + if (org == null) { + return Result.withError(BidderError.badInput("checkExtAndExtractOrg: no org or aid supplied")); } - return Result.of(makeGroupRequests(request, sourceIdToModifiedImp), errors); + + return Result.withValue(BidderUtil.defaultRequest(request, resolveEndpoint(org), mapper)); } private ExtImpOpenweb parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), OPENWEB_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException("ignoring imp id=%s, error while encoding impExt, err: %s" - .formatted(imp.getId(), e.getMessage())); + throw new PreBidException("unmarshal ExtImpOpenWeb: " + e.getMessage()); } } - private Imp modifyImp(Imp imp, ExtImpOpenweb impExt) { - final ObjectNode modifiedImpExt = mapper.mapper().createObjectNode() - .set("openweb", mapper.mapper().valueToTree(impExt)); - final BigDecimal bidFloor = impExt.getBidFloor(); - final BigDecimal resolvedBidFloor = BidderUtil.isValidPrice(bidFloor) - ? bidFloor - : imp.getBidfloor(); - - return imp.toBuilder() - .bidfloor(resolvedBidFloor) - .ext(modifiedImpExt) - .build(); + private static void validateImpExt(ExtImpOpenweb extImpOpenweb) { + if (StringUtils.isBlank(extImpOpenweb.getPlacementId())) { + throw new PreBidException("no placement id supplied"); + } } - private List> makeGroupRequests(BidRequest request, - Map> sourceIdToImps) { - - return sourceIdToImps.entrySet().stream() - .map(impGroupEntry -> makeGroupRequest(request, impGroupEntry.getValue(), impGroupEntry.getKey())) - .toList(); - } + private static String orgFrom(ExtImpOpenweb extImpOpenweb) { + final String org = extImpOpenweb.getOrg(); + if (StringUtils.isNotBlank(org)) { + return StringUtils.trim(org); + } - private HttpRequest makeGroupRequest(BidRequest request, List imps, Integer sourceId) { - final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); - return BidderUtil.defaultRequest(modifiedRequest, resolveEndpoint(sourceId), mapper); + final Integer aid = extImpOpenweb.getAid(); + return aid != null && aid != 0 + ? aid.toString() + : null; } - private String resolveEndpoint(Integer sourceId) { - return "%s?aid=%d".formatted(endpointUrl, sourceId); + private String resolveEndpoint(String org) { + return "%s?publisher_id=%s".formatted(endpointUrl, HttpUtil.encodeUrl(org)); } @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final List errors = new ArrayList<>(); - try { + final List errors = new ArrayList<>(); final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + return Result.of(extractBids(bidResponse, errors), errors); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, BidResponse bidResponse, List errors) { + private List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse, errors); - } - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse, List errors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> toBidderBid(bid, bidResponse, bidRequest.getImp(), errors)) + .filter(Objects::nonNull) + .map(bid -> toBidderBid(bid, bidResponse.getCur(), errors)) .filter(Objects::nonNull) .toList(); } - private BidderBid toBidderBid(Bid bid, BidResponse bidResponse, List imps, List errors) { + private BidderBid toBidderBid(Bid bid, String currency, List errors) { try { - return BidderBid.of(bid, getBidType(bid.getId(), bid.getImpid(), imps), bidResponse.getCur()); + return BidderBid.of(bid, getBidType(bid.getMtype()), currency); } catch (PreBidException e) { errors.add(BidderError.badServerResponse(e.getMessage())); return null; } } - private static BidType getBidType(String bidId, String impId, List imps) { - for (Imp imp : imps) { - if (impId.equals(imp.getId())) { - if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getBanner() != null) { - return BidType.banner; - } - } - } - - throw new PreBidException( - "ignoring bid id=%s, request doesn't contain any impression with id=%s".formatted(bidId, impId)); + private static BidType getBidType(Integer mType) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException("unsupported MType " + mType); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java index 395ab3a7532..2ef79d3bfd4 100644 --- a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java +++ b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java @@ -5,10 +5,9 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; -import org.apache.commons.collections4.MapUtils; -import org.prebid.server.bidder.openx.proto.OpenxBidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -18,6 +17,7 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.openx.model.OpenxImpType; +import org.prebid.server.bidder.openx.proto.OpenxBidResponse; import org.prebid.server.bidder.openx.proto.OpenxBidResponseExt; import org.prebid.server.bidder.openx.proto.OpenxRequestExt; import org.prebid.server.bidder.openx.proto.OpenxVideoExt; @@ -29,6 +29,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.openx.ExtImpOpenx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -169,7 +170,7 @@ private BidRequest createSingleRequest(List imps, BidRequest bidRequest, Li return CollectionUtils.isNotEmpty(processedImps) ? bidRequest.toBuilder() .imp(processedImps) - .ext(makeReqExt(imps.get(0))) + .ext(makeReqExt(imps.getFirst())) .build() : null; } @@ -253,10 +254,26 @@ private static List bidsFromResponse(BidRequest bidRequest, OpenxBidR .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid, impIdToBidType), bidCurrency)) + .map(bid -> toBidderBid(bid, impIdToBidType, bidCurrency)) .toList(); } + private static BidderBid toBidderBid(Bid bid, Map impIdToBidType, String bidCurrency) { + final BidType bidType = getBidType(bid, impIdToBidType); + final ExtBidPrebidVideo videoInfo = bidType == BidType.video ? getVideoInfo(bid) : null; + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(bidCurrency) + .videoInfo(videoInfo) + .build(); + } + + private static ExtBidPrebidVideo getVideoInfo(Bid bid) { + final String primaryCategory = CollectionUtils.isEmpty(bid.getCat()) ? null : bid.getCat().getFirst(); + return ExtBidPrebidVideo.of(bid.getDur(), primaryCategory); + } + private static Map impIdToBidType(BidRequest bidRequest) { return bidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, imp -> imp.getBanner() != null ? BidType.banner : BidType.video)); diff --git a/src/main/java/org/prebid/server/bidder/operaads/OperaadsBidder.java b/src/main/java/org/prebid/server/bidder/operaads/OperaadsBidder.java index 655578064e2..b3ce9f10a17 100644 --- a/src/main/java/org/prebid/server/bidder/operaads/OperaadsBidder.java +++ b/src/main/java/org/prebid/server/bidder/operaads/OperaadsBidder.java @@ -134,7 +134,7 @@ private static Banner modifyBanner(Banner banner) { if (w == null || w == 0 || h == null || h == 0) { if (CollectionUtils.isNotEmpty(formats)) { - final Format firstFormat = formats.get(0); + final Format firstFormat = formats.getFirst(); return banner.toBuilder() .w(firstFormat.getW()) .h(firstFormat.getH()) diff --git a/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java new file mode 100644 index 00000000000..b7e9ff64afa --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.oraki; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.oraki.ExtImpOraki; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class OrakiBidder implements Bidder { + + private static final TypeReference> ORAKI_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OrakiBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpOraki extImpOraki; + try { + extImpOraki = parseImpExt(imp); + outgoingRequests.add(createSingleRequest(modifyImp(imp, extImpOraki), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(outgoingRequests, errors); + } + + private ExtImpOraki parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ORAKI_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpOraki extImpOraki) { + final OrakiImpExtBidder orakiImpExtBidder = getImpExtOrakiWithType(extImpOraki); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(orakiImpExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private OrakiImpExtBidder getImpExtOrakiWithType(ExtImpOraki extImpOraki) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpOraki.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpOraki.getEndpointId()); + + return OrakiImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpOraki.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpOraki.getEndpointId() : null) + .build(); + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} + diff --git a/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java b/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java new file mode 100644 index 00000000000..4c3e8c3b9f5 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.oraki.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class OrakiImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/orbidder/OrbidderBidder.java b/src/main/java/org/prebid/server/bidder/orbidder/OrbidderBidder.java index f87b312d533..4565c91ee28 100644 --- a/src/main/java/org/prebid/server/bidder/orbidder/OrbidderBidder.java +++ b/src/main/java/org/prebid/server/bidder/orbidder/OrbidderBidder.java @@ -143,7 +143,7 @@ private static BidType getBidType(Integer mType) { case 3 -> BidType.audio; case 4 -> BidType.xNative; - default -> throw new PreBidException("Unsupported mType " + mType); + case null, default -> throw new PreBidException("Unsupported mType " + mType); }; } } diff --git a/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java new file mode 100644 index 00000000000..59258985635 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java @@ -0,0 +1,142 @@ +package org.prebid.server.bidder.ownadx; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ownadx.ExtImpOwnAdx; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class OwnAdxBidder implements Bidder { + + private static final TypeReference> OWN_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String X_OPEN_RTB_VERSION = "2.5"; + private static final String SEAT_ID_MACROS_ENDPOINT = "{{SeatID}}"; + private static final String SSP_ID_MACROS_ENDPOINT = "{{SspID}}"; + private static final String TOKEN_ID_MACROS_ENDPOINT = "{{TokenID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OwnAdxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + for (Imp imp : bidRequest.getImp()) { + try { + final ExtImpOwnAdx impOwnAdx = parseImpExt(imp); + httpRequests.add(createHttpRequest(bidRequest, impOwnAdx)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpOwnAdx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), OWN_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private HttpRequest createHttpRequest(BidRequest bidRequest, ExtImpOwnAdx extImpOwnAdx) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImpOwnAdx)) + .headers(makeHeaders()) + .body(mapper.encodeToBytes(bidRequest)) + .impIds(BidderUtil.impIds(bidRequest)) + .payload(bidRequest) + .build(); + } + + private String makeUrl(ExtImpOwnAdx extImpOwnAdx) { + final Optional ownAdx = Optional.ofNullable(extImpOwnAdx); + return endpointUrl + .replace(SEAT_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getSeatId).orElse(StringUtils.EMPTY)) + .replace(SSP_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getSspId).orElse(StringUtils.EMPTY)) + .replace(TOKEN_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getTokenId).orElse(StringUtils.EMPTY)); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPEN_RTB_VERSION); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidMediaType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType " + bid.getMtype()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/playdigo/PlaydigoBidder.java b/src/main/java/org/prebid/server/bidder/playdigo/PlaydigoBidder.java new file mode 100644 index 00000000000..0744bb83d56 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/playdigo/PlaydigoBidder.java @@ -0,0 +1,148 @@ +package org.prebid.server.bidder.playdigo; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.micrometer.common.util.StringUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.playdigo.ExtImpPlaydigo; +import org.prebid.server.proto.openrtb.ext.request.playdigo.PlaydigoImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class PlaydigoBidder implements Bidder { + + private static final TypeReference> PLAYDIGO_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PUBLISHER_PROPERTY = "publisher"; + private static final String NETWORK_PROPERTY = "network"; + private static final String BIDDER_PROPERTY = "bidder"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public PlaydigoBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpPlaydigo extImpPlaydigo; + try { + extImpPlaydigo = parseExtImp(imp); + final Imp modifiedImp = modifyImp(imp, extImpPlaydigo); + httpRequests.add(makeHttpRequest(request, modifiedImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (httpRequests.isEmpty()) { + return Result.withError(BidderError.badInput("found no valid impressions")); + } + + return Result.of(httpRequests, errors); + } + + private ExtImpPlaydigo parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), PLAYDIGO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpPlaydigo extImpPlaydigo) { + final PlaydigoImpExt impExtPlaydigoWithType = resolveImpExt(extImpPlaydigo); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + modifiedImpExtBidder.set(BIDDER_PROPERTY, mapper.mapper().valueToTree(impExtPlaydigoWithType)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private PlaydigoImpExt resolveImpExt(ExtImpPlaydigo extImpPlaydigo) { + final PlaydigoImpExt.PlaydigoImpExtBuilder builder = PlaydigoImpExt.builder(); + + if (StringUtils.isNotEmpty(extImpPlaydigo.getPlacementId())) { + builder.type(PUBLISHER_PROPERTY).placementId(extImpPlaydigo.getPlacementId()); + } else if (StringUtils.isNotEmpty(extImpPlaydigo.getEndpointId())) { + builder.type(NETWORK_PROPERTY).endpointId(extImpPlaydigo.getEndpointId()); + } + + return builder.build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, Imp imp) { + final BidRequest outgoingRequest = request.toBuilder().imp(List.of(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse); + return Result.withValues(bids); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private BidType getBidType(Bid bid) { + final Integer markupType = ObjectUtils.defaultIfNull(bid.getMtype(), 0); + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/preciso/PrecisoBidder.java b/src/main/java/org/prebid/server/bidder/preciso/PrecisoBidder.java index 1e8012b4720..3c29aa13b6d 100644 --- a/src/main/java/org/prebid/server/bidder/preciso/PrecisoBidder.java +++ b/src/main/java/org/prebid/server/bidder/preciso/PrecisoBidder.java @@ -71,7 +71,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ } } - if (errors.size() > 0) { + if (!errors.isEmpty()) { return Result.withErrors(errors); } @@ -126,7 +126,9 @@ private Price resolveBidFloor(Imp imp, ExtImpPreciso impExt, BidRequest bidReque final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); final BigDecimal impExtBidFloor = impExt.getBidFloor(); - final String impExtCurrency = impExtBidFloor != null && brCur != null && brCur.size() > 0 ? brCur.get(0) : null; + final String impExtCurrency = impExtBidFloor != null && brCur != null && !brCur.isEmpty() + ? brCur.getFirst() + : null; final Price impExtBidFloorPrice = Price.of(impExtCurrency, impExtBidFloor); final Price resolvedPrice = initialBidFloorPrice.getValue() == null ? impExtBidFloorPrice : initialBidFloorPrice; diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java index 7de057072f3..a60a6ab2efa 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -13,7 +13,6 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.compress.utils.Lists; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; @@ -36,24 +35,26 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; +import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.StreamUtil; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; public class PubmaticBidder implements Bidder { @@ -67,6 +68,7 @@ public class PubmaticBidder implements Bidder { private static final String WRAPPER_EXT_REQUEST = "wrapper"; private static final String BIDDER_NAME = "pubmatic"; private static final String AE = "ae"; + private static final String GP_ID = "gpid"; private static final String IMP_EXT_PBADSLOT = "pbadslot"; private static final String IMP_EXT_ADSERVER = "adserver"; private static final List IMP_EXT_DATA_RESERVED_FIELD = List.of(IMP_EXT_PBADSLOT, IMP_EXT_ADSERVER); @@ -102,8 +104,9 @@ public Result>> makeHttpRequests(BidRequest request final PubmaticBidderImpExt impExt = parseImpExt(imp); final ExtImpPubmatic extImpPubmatic = impExt.getBidder(); - publisherId = ObjectUtils.defaultIfNull( - publisherId, StringUtils.trimToNull(extImpPubmatic.getPublisherId())); + if (publisherId == null) { + publisherId = StringUtils.trimToNull(extImpPubmatic.getPublisherId()); + } wrapper = merge(wrapper, extImpPubmatic.getWrapper()); @@ -156,6 +159,14 @@ private static void validateMediaType(Imp imp) { } } + private PubmaticBidderImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), PubmaticBidderImpExt.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + private static PubmaticWrapper merge(PubmaticWrapper left, PubmaticWrapper right) { if (Objects.equals(left, right) || isWrapperValid(left)) { return left; @@ -180,30 +191,37 @@ private static Integer stripToNull(Integer value) { return value == null || value == 0 ? null : value; } - private PubmaticBidderImpExt parseImpExt(Imp imp) { - try { - return mapper.mapper().convertValue(imp.getExt(), PubmaticBidderImpExt.class); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - } - private Imp modifyImp(Imp imp, PubmaticBidderImpExt impExt) { final Banner banner = imp.getBanner(); final ExtImpPubmatic impExtBidder = impExt.getBidder(); - final ObjectNode modifiedExt = makeKeywords(impExt); - if (impExt.getAe() != null) { - modifiedExt.put(AE, impExt.getAe()); - } + final ObjectNode newExt = makeKeywords(impExt); final Imp.ImpBuilder impBuilder = imp.toBuilder() .banner(banner != null ? assignSizesIfMissing(banner) : null) - .ext(!modifiedExt.isEmpty() ? modifiedExt : null) + .audio(null) .bidfloor(resolveBidFloor(impExtBidder.getKadfloor(), imp.getBidfloor())) - .audio(null); + .ext(!newExt.isEmpty() ? newExt : null); - return enrichWithAdSlotParameters(impBuilder, impExtBidder.getAdSlot(), banner).build(); + enrichWithAdSlotParameters(impBuilder, impExtBidder.getAdSlot(), banner); + + return impBuilder.build(); + } + + private static Banner assignSizesIfMissing(Banner banner) { + final List format = banner.getFormat(); + if ((banner.getW() != null && banner.getH() != null) || CollectionUtils.isEmpty(format)) { + return banner; + } + + final Format firstFormat = format.getFirst(); + return modifyWithSizeParams(banner, firstFormat.getW(), firstFormat.getH()); + } + + private static Banner modifyWithSizeParams(Banner banner, Integer width, Integer height) { + return banner != null + ? banner.toBuilder().w(width).h(height).build() + : null; } private BigDecimal resolveBidFloor(String kadfloor, BigDecimal existingFloor) { @@ -214,6 +232,9 @@ private BigDecimal resolveBidFloor(String kadfloor, BigDecimal existingFloor) { } private static BigDecimal parseKadFloor(String kadFloorString) { + if (StringUtils.isBlank(kadFloorString)) { + return null; + } try { return new BigDecimal(StringUtils.trimToEmpty(kadFloorString)); } catch (NumberFormatException e) { @@ -221,139 +242,99 @@ private static BigDecimal parseKadFloor(String kadFloorString) { } } - private static Imp.ImpBuilder enrichWithAdSlotParameters(Imp.ImpBuilder impBuilder, String adSlot, Banner banner) { - final String trimmedAdSlot = StringUtils.trimToNull(adSlot); - - if (StringUtils.isEmpty(trimmedAdSlot)) { - return impBuilder; - } - - if (!trimmedAdSlot.contains("@")) { - impBuilder.tagid(trimmedAdSlot); - return impBuilder; - } - - final String[] adSlotParams = trimmedAdSlot.split("@"); - if (adSlotParams.length != 2 - || StringUtils.isEmpty(adSlotParams[0].trim()) - || StringUtils.isEmpty(adSlotParams[1].trim())) { - throw new PreBidException("Invalid adSlot '%s'".formatted(trimmedAdSlot)); - } - - impBuilder.tagid(adSlotParams[0]); - - final String[] adSize = adSlotParams[1].toLowerCase().split("x"); - if (adSize.length != 2) { - throw new PreBidException("Invalid size provided in adSlot '%s'".formatted(trimmedAdSlot)); - } - - final Integer width = parseAdSizeParam(adSize[0], "width", adSlot); - final String[] heightParams = adSize[1].split(":"); - final Integer height = parseAdSizeParam(heightParams[0], "height", adSlot); + private ObjectNode makeKeywords(PubmaticBidderImpExt impExt) { + final ObjectNode keywordsNode = mapper.mapper().createObjectNode(); - return impBuilder.banner(modifyWithSizeParams(banner, width, height)); - } + final ExtImpPubmatic extBidder = impExt.getBidder(); + putExtBidderKeywords(keywordsNode, extBidder); + putExtDataKeywords(keywordsNode, impExt.getData(), extBidder.getDctr()); - private static Integer parseAdSizeParam(String number, String paramName, String adSlot) { - try { - return Integer.parseInt(number.trim()); - } catch (NumberFormatException e) { - throw new PreBidException("Invalid %s provided in adSlot '%s'".formatted(paramName, adSlot)); + if (impExt.getAe() != null) { + keywordsNode.put(AE, impExt.getAe()); } - } - - private static Banner modifyWithSizeParams(Banner banner, Integer width, Integer height) { - return banner != null - ? banner.toBuilder().w(width).h(height).build() - : null; - } - - private static Banner assignSizesIfMissing(Banner banner) { - final List format = banner.getFormat(); - if ((banner.getW() != null && banner.getH() != null) || CollectionUtils.isEmpty(format)) { - return banner; + if (impExt.getGpId() != null) { + keywordsNode.put(GP_ID, impExt.getGpId()); } - final Format firstFormat = format.get(0); - - return modifyWithSizeParams(banner, firstFormat.getW(), firstFormat.getH()); - } - - private ObjectNode makeKeywords(PubmaticBidderImpExt impExt) { - final ObjectNode keywordsNode = mapper.mapper().createObjectNode(); - putExtBidderKeywords(keywordsNode, impExt.getBidder()); - putExtDataKeywords(keywordsNode, impExt.getData(), impExt.getBidder()); - return keywordsNode; } private static void putExtBidderKeywords(ObjectNode keywords, ExtImpPubmatic extBidder) { - CollectionUtils.emptyIfNull(extBidder.getKeywords()).forEach(keyword -> { + for (ExtImpPubmaticKeyVal keyword : CollectionUtils.emptyIfNull(extBidder.getKeywords())) { if (CollectionUtils.isEmpty(keyword.getValue())) { - return; + continue; } keywords.put(keyword.getKey(), String.join(",", keyword.getValue())); - }); + } + final JsonNode pmZoneIdKeyWords = keywords.remove(PM_ZONE_ID_OLD_KEY_NAME); final String pmZomeId = extBidder.getPmZoneId(); if (StringUtils.isNotEmpty(pmZomeId)) { - keywords.put(PM_ZONE_ID_KEY_NAME, extBidder.getPmZoneId()); + keywords.put(PM_ZONE_ID_KEY_NAME, pmZomeId); } else if (pmZoneIdKeyWords != null) { keywords.set(PM_ZONE_ID_KEY_NAME, pmZoneIdKeyWords); } } - private void putExtDataKeywords(ObjectNode keywords, ObjectNode extData, ExtImpPubmatic extBidder) { - final List dctrValues = new ArrayList<>(); - - final String dctr = extBidder.getDctr(); - if (StringUtils.isNotEmpty(dctr)) { - dctrValues.add(dctr); + private void putExtDataKeywords(ObjectNode keywords, ObjectNode extData, String dctr) { + final String newDctr = extractDctr(dctr, extData); + if (StringUtils.isNotEmpty(newDctr)) { + keywords.put(DCTR_KEY_NAME, newDctr); } - if (extData != null) { - final String pbaAdSlot = Optional.ofNullable(extData.get(IMP_EXT_PBADSLOT)) - .map(JsonNode::asText) - .orElse(null); - final PubmaticExtDataAdServer extAdServer = extractAdServer(extData); - final String adServerName = extAdServer != null ? extAdServer.getName() : null; - final String adServerAdSlot = extAdServer != null ? extAdServer.getAdSlot() : null; - if (AD_SERVER_GAM.equals(adServerName) && StringUtils.isNotEmpty(adServerAdSlot)) { - keywords.put(IMP_EXT_AD_UNIT_KEY, adServerAdSlot); - } else if (StringUtils.isNotEmpty(pbaAdSlot)) { - keywords.put(IMP_EXT_AD_UNIT_KEY, pbaAdSlot); - } - - dctrValues.addAll(extractDctrValues(extData)); + final String adUnitCode = extractAdUnitCode(extData); + if (StringUtils.isNotEmpty(adUnitCode)) { + keywords.put(IMP_EXT_AD_UNIT_KEY, adUnitCode); } + } - if (!dctrValues.isEmpty()) { - keywords.put(DCTR_KEY_NAME, String.join("|", dctrValues)); + private static String extractDctr(String firstDctr, ObjectNode extData) { + if (extData == null) { + return firstDctr; } + + return Stream.concat( + Stream.of(firstDctr), + StreamUtil.asStream(extData.fields()) + .filter(entry -> !IMP_EXT_DATA_RESERVED_FIELD.contains(entry.getKey())) + .map(PubmaticBidder::buildDctrPart)) + .filter(Objects::nonNull) + .collect(Collectors.joining("|")); } - private static List extractDctrValues(ObjectNode extData) { - final List dctrValues = new ArrayList<>(); - final Iterator> extDataIterator = extData.fields(); - while (extDataIterator.hasNext()) { - final Map.Entry entry = extDataIterator.next(); - final String key = entry.getKey(); - if (IMP_EXT_DATA_RESERVED_FIELD.contains(key)) { - continue; - } + private static String buildDctrPart(Map.Entry dctrPart) { + final JsonNode value = dctrPart.getValue(); + final String valueAsString = value.isValueNode() + ? StringUtils.trim(value.asText()) + : null; + final String arrayAsString = valueAsString == null && value.isArray() + ? StreamUtil.asStream(value.elements()) + .map(JsonNode::asText) + .map(StringUtils::trim) + .collect(Collectors.joining(",")) + : null; - final JsonNode value = entry.getValue(); - if (value.isValueNode()) { - dctrValues.add(DCTR_VALUE_FORMAT.formatted(key, StringUtils.trim(value.asText()))); - } else if (value.isArray()) { - final String arrayNodeValue = Lists.newArrayList(value.elements()).stream() - .map(JsonNode::asText) - .collect(Collectors.joining(",")); - dctrValues.add(DCTR_VALUE_FORMAT.formatted(key, arrayNodeValue)); - } + final String valuePart = ObjectUtils.firstNonNull(valueAsString, arrayAsString); + + return valuePart != null + ? DCTR_VALUE_FORMAT.formatted(StringUtils.trim(dctrPart.getKey()), valuePart) + : null; + } + + private String extractAdUnitCode(ObjectNode extData) { + if (extData == null) { + return null; } - return dctrValues; + final PubmaticExtDataAdServer extAdServer = extractAdServer(extData); + final String adServerName = extAdServer != null ? extAdServer.getName() : null; + final String adServerAdSlot = extAdServer != null ? extAdServer.getAdSlot() : null; + + return AD_SERVER_GAM.equals(adServerName) && StringUtils.isNotEmpty(adServerAdSlot) + ? adServerAdSlot + : Optional.ofNullable(extData.get(IMP_EXT_PBADSLOT)) + .map(JsonNode::asText) + .orElse(null); } private PubmaticExtDataAdServer extractAdServer(ObjectNode extData) { @@ -364,8 +345,49 @@ private PubmaticExtDataAdServer extractAdServer(ObjectNode extData) { } } - private HttpRequest makeHttpRequest(BidRequest request) { - return BidderUtil.defaultRequest(request, endpointUrl, mapper); + private static void enrichWithAdSlotParameters(Imp.ImpBuilder impBuilder, String adSlot, Banner banner) { + final String trimmedAdSlot = StringUtils.trimToNull(adSlot); + if (StringUtils.isEmpty(trimmedAdSlot)) { + return; + } + + if (!trimmedAdSlot.contains("@")) { + impBuilder.tagid(trimmedAdSlot); + return; + } + + final String[] adSlotParams = trimmedAdSlot.split("@"); + final String trimmedParam0 = adSlotParams.length == 2 ? adSlotParams[0].trim() : null; + final String trimmedParam1 = adSlotParams.length == 2 ? adSlotParams[1].trim() : null; + + if (adSlotParams.length != 2 + || StringUtils.isEmpty(trimmedParam0) + || StringUtils.isEmpty(trimmedParam1)) { + + throw new PreBidException("Invalid adSlot '%s'".formatted(trimmedAdSlot)); + } + + impBuilder.tagid(trimmedParam0); + + final String[] adSize = trimmedParam1.toLowerCase().split("x"); + if (adSize.length != 2) { + throw new PreBidException("Invalid size provided in adSlot '%s'".formatted(trimmedAdSlot)); + } + + final Integer width = parseAdSizeParam(adSize[0], "width", adSlot); + + final String[] heightParams = adSize[1].split(":"); + final Integer height = parseAdSizeParam(heightParams[0], "height", adSlot); + + impBuilder.banner(modifyWithSizeParams(banner, width, height)); + } + + private static Integer parseAdSizeParam(String number, String paramName, String adSlot) { + try { + return Integer.parseInt(number.trim()); + } catch (NumberFormatException e) { + throw new PreBidException("Invalid %s provided in adSlot '%s'".formatted(paramName, adSlot)); + } } private BidRequest modifyBidRequest(BidRequest request, @@ -376,28 +398,12 @@ private BidRequest modifyBidRequest(BidRequest request, return request.toBuilder() .imp(imps) - .app(modifyApp(request.getApp(), publisherId)) .site(modifySite(request.getSite(), publisherId)) + .app(modifyApp(request.getApp(), publisherId)) .ext(modifyExtRequest(request.getExt(), wrapper, acat)) .build(); } - private ExtRequest modifyExtRequest(ExtRequest extRequest, PubmaticWrapper wrapper, List acat) { - final ObjectNode extNode = mapper.mapper().createObjectNode(); - - if (wrapper != null) { - extNode.set(WRAPPER_EXT_REQUEST, mapper.mapper().valueToTree(wrapper)); - } - - if (CollectionUtils.isNotEmpty(acat)) { - extNode.set(ACAT_EXT_REQUEST, mapper.mapper().valueToTree(acat)); - } - - return extNode.isEmpty() - ? extRequest - : mapper.fillExtension(extRequest == null ? ExtRequest.empty() : extRequest, extNode); - } - private static Site modifySite(Site site, String publisherId) { return publisherId != null && site != null ? site.toBuilder() @@ -420,6 +426,26 @@ private static Publisher modifyPublisher(Publisher publisher, String publisherId : Publisher.builder().id(publisherId).build(); } + private ExtRequest modifyExtRequest(ExtRequest extRequest, PubmaticWrapper wrapper, List acat) { + final ObjectNode extNode = mapper.mapper().createObjectNode(); + + if (wrapper != null) { + extNode.putPOJO(WRAPPER_EXT_REQUEST, wrapper); + } + + if (CollectionUtils.isNotEmpty(acat)) { + extNode.putPOJO(ACAT_EXT_REQUEST, acat); + } + + return extNode.isEmpty() + ? extRequest + : mapper.fillExtension(extRequest == null ? ExtRequest.empty() : extRequest, extNode); + } + + private HttpRequest makeHttpRequest(BidRequest request) { + return BidderUtil.defaultRequest(request, endpointUrl, mapper); + } + /** * @deprecated for this bidder in favor of @link{makeBidderResponse} which supports additional response data */ @@ -459,22 +485,24 @@ private List bidsFromResponse(PubmaticBidResponse bidResponse, List bidderErrors) { - final List singleElementBidCat = CollectionUtils.emptyIfNull(bid.getCat()).stream() - .limit(1) - .collect(Collectors.collectingAndThen(Collectors.toList(), - bidCat -> !bidCat.isEmpty() ? bidCat : null)); + final List cat = bid.getCat(); + final List firstCat = CollectionUtils.isNotEmpty(cat) + ? Collections.singletonList(cat.getFirst()) + : null; - final PubmaticBidExt pubmaticBidExt = extractBidExt(bid.getExt()); + final PubmaticBidExt pubmaticBidExt = parseBidExt(bid.getExt(), bidderErrors); final Integer duration = getDuration(pubmaticBidExt); final BidType bidType = getBidType(pubmaticBidExt); + final String bidAdm = bid.getAdm(); final String resolvedAdm = bidAdm != null && bidType == BidType.xNative ? resolveNativeAdm(bidAdm, bidderErrors) : bidAdm; - final Bid updatedBid = singleElementBidCat != null || duration != null || resolvedAdm != null + + final Bid updatedBid = firstCat != null || duration != null || resolvedAdm != null ? bid.toBuilder() + .cat(firstCat) .adm(resolvedAdm != null ? resolvedAdm : bidAdm) - .cat(singleElementBidCat) .ext(duration != null ? updateBidExtWithExtPrebid(duration, bid.getExt()) : bid.getExt()) .build() : bid; @@ -487,18 +515,26 @@ private BidderBid resolveBidderBid(Bid bid, String currency, List b .build(); } - private PubmaticBidExt extractBidExt(ObjectNode bidExt) { + private PubmaticBidExt parseBidExt(ObjectNode bidExt, List errors) { try { return bidExt != null ? mapper.mapper().treeToValue(bidExt, PubmaticBidExt.class) : null; } catch (JsonProcessingException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); return null; } } + private static Integer getDuration(PubmaticBidExt bidExt) { + return Optional.ofNullable(bidExt) + .map(PubmaticBidExt::getVideo) + .map(VideoCreativeInfo::getDuration) + .orElse(null); + } + private static BidType getBidType(PubmaticBidExt bidExt) { - final Integer bidType = bidExt != null - ? ObjectUtils.defaultIfNull(bidExt.getBidType(), 0) - : 0; + final int bidType = Optional.ofNullable(bidExt) + .map(PubmaticBidExt::getBidType) + .orElse(0); return switch (bidType) { case 1 -> BidType.video; @@ -524,11 +560,6 @@ private String resolveNativeAdm(String adm, List bidderErrors) { return null; } - private static Integer getDuration(PubmaticBidExt bidExt) { - final VideoCreativeInfo creativeInfo = bidExt != null ? bidExt.getVideo() : null; - return creativeInfo != null ? creativeInfo.getDuration() : null; - } - private ObjectNode updateBidExtWithExtPrebid(Integer duration, ObjectNode extBid) { final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().video(ExtBidPrebidVideo.of(duration, null)).build(); return extBid.set(PREBID, mapper.mapper().valueToTree(extBidPrebid)); diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java index 0e0976a94d5..bdfd08a9dae 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.pubmatic.model.request; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; @@ -13,4 +14,6 @@ public class PubmaticBidderImpExt { Integer ae; + @JsonProperty("gpid") + String gpId; } diff --git a/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java b/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java index c90d350705c..c536066ab27 100644 --- a/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java @@ -121,7 +121,7 @@ private static Banner resolveBanner(Banner banner) { throw new PreBidException("Size information missing for banner"); } - final Format firstFormat = formats.get(0); + final Format firstFormat = formats.getFirst(); return banner.toBuilder() .w(firstFormat.getW()) .h(firstFormat.getH()) @@ -148,7 +148,7 @@ private static String resolveBidFloorCurrency(BidRequest bidRequest, String bidF return bidFloorCurrency; } final List bidRequestCurrencies = bidRequest.getCur(); - return CollectionUtils.isNotEmpty(bidRequestCurrencies) ? bidRequestCurrencies.get(0) : null; + return CollectionUtils.isNotEmpty(bidRequestCurrencies) ? bidRequestCurrencies.getFirst() : null; } private HttpRequest createHttpRequest(BidRequest outgoingRequest, ExtImpPubnative impExt) { @@ -234,13 +234,14 @@ private static Format resolveBidSizeFromBanner(Banner banner) { ? Format.builder().w(width).h(height).build() : null; } else if (formats.size() == 1) { - result = formats.get(0); + result = formats.getFirst(); } return result; } private static boolean isOnlyOneSize(Integer width, Integer height, List formats) { - return CollectionUtils.isEmpty(formats) || (formats.size() == 1 && isSameFormat(width, height, formats.get(0))); + return CollectionUtils.isEmpty(formats) + || (formats.size() == 1 && isSameFormat(width, height, formats.getFirst())); } private static boolean isSameFormat(Integer width, Integer height, Format format) { diff --git a/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java b/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java new file mode 100644 index 00000000000..f8fe37197b0 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.pubrise; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.pubrise.proto.PubriseImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.pubrise.ExtImpPubrise; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class PubriseBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public PubriseBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpPubrise extImp; + try { + extImp = parseImpExt(imp); + outgoingRequests.add(makeRequest(modifyImp(imp, extImp), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(outgoingRequests) + ? Result.withError(BidderError.badInput("found no valid impressions")) + : Result.of(outgoingRequests, errors); + } + + private ExtImpPubrise parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpPubrise extImp) { + final PubriseImpExtBidder impExtBidder = getImpExtWithType(extImp); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(impExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private PubriseImpExtBidder getImpExtWithType(ExtImpPubrise extImpQt) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpQt.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpQt.getEndpointId()); + + return PubriseImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpQt.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpQt.getEndpointId() : null) + .build(); + } + + private HttpRequest makeRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java b/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java new file mode 100644 index 00000000000..2cb89d4d287 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.pubrise.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class PubriseImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/qt/QtBidder.java b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java new file mode 100644 index 00000000000..4b07ff86e97 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java @@ -0,0 +1,137 @@ +package org.prebid.server.bidder.qt; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.qt.ExtImpQt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class QtBidder implements Bidder { + + private static final TypeReference> QT_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public QtBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpQt extImpQt; + try { + extImpQt = parseImpExt(imp); + outgoingRequests.add(createSingleRequest(modifyImp(imp, extImpQt), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(outgoingRequests, errors); + } + + private ExtImpQt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), QT_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpQt extImpQt) { + final QtImpExtBidder qtImpExtBidder = getImpExtQtWithType(extImpQt); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(qtImpExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private QtImpExtBidder getImpExtQtWithType(ExtImpQt extImpQt) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpQt.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpQt.getEndpointId()); + + return QtImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpQt.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpQt.getEndpointId() : null) + .build(); + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java b/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java new file mode 100644 index 00000000000..4f9abd708c4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.qt.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class QtImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/readpeak/ReadPeakBidder.java b/src/main/java/org/prebid/server/bidder/readpeak/ReadPeakBidder.java new file mode 100644 index 00000000000..a24cbf044c6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/readpeak/ReadPeakBidder.java @@ -0,0 +1,223 @@ +package org.prebid.server.bidder.readpeak; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.readpeak.ExtImpReadPeak; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class ReadPeakBidder implements Bidder { + + private static final TypeReference> READPEAK_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ReadPeakBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + ExtImpReadPeak extImp = null; + for (Imp imp : request.getImp()) { + try { + extImp = parseImpExt(imp); + final Imp modifiedImp = modifyImp(imp, extImp); + modifiedImps.add(modifiedImp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (modifiedImps.isEmpty()) { + return Result.withError(BidderError.badInput( + String.format("Failed to find compatible impressions for request %s", request.getId()))); + } + + final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); + final HttpRequest httpRequest = makeHttpRequest(modifiedRequest, extImp); + + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private ExtImpReadPeak parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), READPEAK_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize ReadPeak extension: " + e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpReadPeak extImpReadPeak) { + return imp.toBuilder() + .bidfloor(extImpReadPeak.getBidFloor() != null ? extImpReadPeak.getBidFloor() : imp.getBidfloor()) + .tagid(StringUtils.isNotBlank(extImpReadPeak.getTagId()) ? extImpReadPeak.getTagId() : imp.getTagid()) + .build(); + } + + private static Site modifySite(Site site, String siteId, Publisher publisher) { + return site.toBuilder() + .id(StringUtils.isNotBlank(siteId) ? siteId : site.getId()) + .publisher(publisher) + .build(); + } + + private static App modifyApp(App app, ExtImpReadPeak extImp, Publisher publisher) { + return app.toBuilder() + .id(StringUtils.isNotBlank(extImp.getSiteId()) ? extImp.getSiteId() : app.getId()) + .publisher(publisher) + .build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, ExtImpReadPeak extImp) { + final Publisher publisher = Publisher.builder().id(extImp.getPublisherId()).build(); + + final boolean hasSite = request.getSite() != null; + final boolean hasApp = !hasSite && request.getApp() != null; + + final BidRequest outgoingRequest = request.toBuilder() + .site(hasSite ? modifySite(request.getSite(), extImp.getSiteId(), publisher) : null) + .app(hasApp ? modifyApp(request.getApp(), extImp, publisher) : null) + .build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + try { + final Bid resolvedBid = resolveMacros(bid); + final BidType bidType = getBidType(bid); + final Bid updatedBid = addBidMeta(resolvedBid); + return BidderBid.of(updatedBid, bidType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + + } + + private static Bid resolveMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + return bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .burl(StringUtils.replace(bid.getBurl(), PRICE_MACRO, priceAsString)) + .build(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = ObjectUtils.defaultIfNull(bid.getMtype(), 0); + + return switch (markupType) { + case 1 -> BidType.banner; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid()); + }; + } + + private Bid addBidMeta(Bid bid) { + final ExtBidPrebid prebid = parseExtBidPrebid(bid); + + final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid).map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .advertiserDomains(bid.getAdomain()) + .build(); + + final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid) + .map(ExtBidPrebid::toBuilder) + .orElseGet(ExtBidPrebid::builder) + .meta(modifiedMeta) + .build(); + + return bid.toBuilder() + .ext(mapper.mapper().valueToTree(ExtPrebid.of(modifiedPrebid, null))) + .build(); + } + + private ExtBidPrebid parseExtBidPrebid(Bid bid) { + try { + return Optional.ofNullable(mapper.mapper().convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE)) + .map(ExtPrebid::getPrebid) + .orElse(null); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/roulax/RoulaxBidder.java b/src/main/java/org/prebid/server/bidder/roulax/RoulaxBidder.java new file mode 100644 index 00000000000..d21dd3f4b68 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/roulax/RoulaxBidder.java @@ -0,0 +1,122 @@ +package org.prebid.server.bidder.roulax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.roulax.ExtImpRoulax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class RoulaxBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + private static final String PUBLISHER_PATH_MACRO = "{{PublisherID}}"; + private static final String ACCOUNT_ID_MACRO = "{{AccountID}}"; + + private static final TypeReference> ROULAX_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + public RoulaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpRoulax extImpRoulax = parseImpExt(bidRequest.getImp().getFirst()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, resolveEndpoint(extImpRoulax), mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpRoulax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ROULAX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize Roulax extension: " + e.getMessage()); + } + } + + private String resolveEndpoint(ExtImpRoulax extImpRoulax) { + return endpointUrl + .replace(PUBLISHER_PATH_MACRO, StringUtils.defaultString(extImpRoulax.getPublisherPath()).trim()) + .replace(ACCOUNT_ID_MACRO, StringUtils.defaultString(extImpRoulax.getPid()).trim()); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType in impID: %s, mType: %d".formatted(bid.getImpid(), bid.getMtype())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java index 311e2e1ee69..39c269b163b 100644 --- a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java +++ b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java @@ -5,9 +5,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import com.iab.openrtb.response.Bid; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; @@ -41,6 +41,7 @@ public class RtbhouseBidder implements Bidder { new TypeReference<>() { }; private static final String BIDDER_CURRENCY = "USD"; + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; private final String endpointUrl; private final JacksonMapper mapper; @@ -71,7 +72,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ } } - if (errors.size() > 0) { + if (!errors.isEmpty()) { return Result.withErrors(errors); } @@ -127,7 +128,7 @@ private BidderBid resolveBidderBid(Bid bid, .build(); return BidderBid.builder() - .bid(updatedBid) + .bid(resolveMacros(updatedBid)) .type(bidType) .bidCurrency(currency) .build(); @@ -157,6 +158,8 @@ private static BidType getBidType(String impId, List imps) { return BidType.banner; } else if (imp.getXNative() != null) { return BidType.xNative; + } else if (imp.getVideo() != null) { + return BidType.video; } } } @@ -184,8 +187,8 @@ private Price resolveBidFloor(Imp imp, ExtImpRtbhouse impExt, BidRequest bidRequ final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); final BigDecimal impExtBidFloor = impExt.getBidFloor(); - final String impExtCurrency = impExtBidFloor != null && brCur != null && brCur.size() > 0 - ? brCur.get(0) : null; + final String impExtCurrency = impExtBidFloor != null && brCur != null && !brCur.isEmpty() + ? brCur.getFirst() : null; final Price impExtBidFloorPrice = Price.of(impExtCurrency, impExtBidFloor); final Price resolvedPrice = initialBidFloorPrice.getValue() == null ? impExtBidFloorPrice : initialBidFloorPrice; @@ -210,4 +213,14 @@ private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidR } } + private static Bid resolveMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + return bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .build(); + } + } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 4f4e8b455b3..031f7590475 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -29,9 +29,8 @@ import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; @@ -80,13 +79,14 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtDeal; import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; -import org.prebid.server.proto.openrtb.ext.request.ExtImpContext; import org.prebid.server.proto.openrtb.ext.request.ExtImpContextDataAdserver; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; @@ -108,15 +108,19 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ListUtil; import org.prebid.server.util.ObjectUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.net.URISyntaxException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -140,16 +144,9 @@ public class RubiconBidder implements Bidder { private static final String TK_XINT_QUERY_PARAMETER = "tk_xint"; private static final String PREBID_SERVER_USER_AGENT = "prebid-server/1.0"; - private static final String SOURCE_RUBICON = "rubiconproject.com"; - private static final String FPD_GPID_FIELD = "gpid"; private static final String FPD_SKADN_FIELD = "skadn"; - private static final String FPD_SECTIONCAT_FIELD = "sectioncat"; - private static final String FPD_PAGECAT_FIELD = "pagecat"; private static final String FPD_PAGE_FIELD = "page"; - private static final String FPD_REF_FIELD = "ref"; - private static final String FPD_SEARCH_FIELD = "search"; - private static final String FPD_CONTEXT_FIELD = "context"; private static final String FPD_DATA_FIELD = "data"; private static final String FPD_DATA_PBADSLOT_FIELD = "pbadslot"; private static final String FPD_ADSERVER_FIELD = "adserver"; @@ -158,13 +155,20 @@ public class RubiconBidder implements Bidder { private static final String DFP_ADUNIT_CODE_FIELD = "dfp_ad_unit_code"; private static final String STYPE_FIELD = "stype"; private static final String PREBID_EXT = "prebid"; + private static final String PBS_LOGIN = "pbs_login"; + private static final String PBS_VERSION = "pbs_version"; + private static final String PBS_URL = "pbs_url"; private static final String PPUID_STYPE = "ppuid"; - private static final String OTHER_STYPE = "other"; private static final String SHA256EMAIL_STYPE = "sha256email"; private static final String DMP_STYPE = "dmp"; private static final String XAPI_CURRENCY = "USD"; + private static final int MAX_NUMBER_OF_SEGMENTS = 100; + private static final String SEGTAX_IAB = "iab"; + private static final String SEGTAX_TAX = "tax"; + private static final String SEGTAX = "segtax"; + private static final Set USER_SEGTAXES = Set.of(4); private static final Set SITE_SEGTAXES = Set.of(1, 2, 5, 6); @@ -178,29 +182,40 @@ public class RubiconBidder implements Bidder { }; private static final boolean DEFAULT_MULTIFORMAT_VALUE = false; + private final String bidderName; private final String endpointUrl; + private final String externalUrl; + private final String xapiUsername; private final Set supportedVendors; private final boolean generateBidId; private final CurrencyConversionService currencyConversionService; private final PriceFloorResolver floorResolver; + private final PrebidVersionProvider versionProvider; private final JacksonMapper mapper; private final MultiMap headers; - public RubiconBidder(String endpoint, + public RubiconBidder(String bidderName, + String endpoint, + String externalUrl, String xapiUsername, String xapiPassword, List supportedVendors, boolean generateBidId, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, JacksonMapper mapper) { + this.bidderName = Objects.requireNonNull(bidderName); this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.externalUrl = HttpUtil.validateUrl(Objects.requireNonNull(externalUrl)); + this.xapiUsername = Objects.requireNonNull(xapiUsername); this.supportedVendors = Set.copyOf(Objects.requireNonNull(supportedVendors)); this.generateBidId = generateBidId; this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.floorResolver = Objects.requireNonNull(floorResolver); + this.versionProvider = Objects.requireNonNull(versionProvider); this.mapper = Objects.requireNonNull(mapper); headers = headers(Objects.requireNonNull(xapiUsername), Objects.requireNonNull(xapiPassword)); @@ -318,7 +333,7 @@ public Map extractTargeting(ObjectNode extBidBidder) { return targetings != null ? targetings.stream() .filter(targeting -> !CollectionUtils.isEmpty(targeting.getValues())) - .collect(Collectors.toMap(RubiconTargeting::getKey, targeting -> targeting.getValues().get(0))) + .collect(Collectors.toMap(RubiconTargeting::getKey, targeting -> targeting.getValues().getFirst())) : Collections.emptyMap(); } @@ -420,7 +435,7 @@ private BidRequest createSingleRequest(BidRequest bidRequest, .device(makeDevice(bidRequest.getDevice())) .site(makeSite(bidRequest.getSite(), impLanguage, extImpRubicon)) .app(makeApp(bidRequest.getApp(), extImpRubicon)) - .source(makeSource(bidRequest.getSource(), extImpRubicon.getPchain())) + .source(makeSource(bidRequest.getSource())) .cur(null) // suppress currencies .regs(makeRegs(bidRequest.getRegs())) .ext(null) // suppress ext @@ -543,6 +558,7 @@ private PriceFloorResult resolvePriceFloors(BidRequest bidRequest, imp, mediaType, null, + bidderName, warnings); } @@ -599,7 +615,7 @@ private BigDecimal convertToXAPICurrency(BigDecimal value, private static BigDecimal resolveBidFloorPrice(Imp imp) { final BigDecimal bidFloor = imp.getBidfloor(); - return BidderUtil.isValidPrice(bidFloor) ? bidFloor : null; + return bidFloor != null && bidFloor.compareTo(BigDecimal.ZERO) >= 0 ? bidFloor : null; } private static String resolveBidFloorCurrency(Imp imp, BidRequest bidRequest, List errors) { @@ -664,7 +680,6 @@ private RubiconImpExt makeImpExt(Imp imp, String ipfResolvedCurrency, PriceFloorResult priceFloorResult) { - final ExtImpContext context = extImpContext(imp); final RubiconImpExtPrebid rubiconImpExtPrebid = priceFloorResult != null ? makeRubiconExtPrebid(priceFloorResult, ipfResolvedCurrency, imp, bidRequest) : null; @@ -675,7 +690,7 @@ private RubiconImpExt makeImpExt(Imp imp, final RubiconImpExtRp rubiconImpExtRp = RubiconImpExtRp.of( rubiconImpExt.getZoneId(), - makeTarget(imp, rubiconImpExt, site, app, context), + makeTarget(imp, rubiconImpExt, site, app), RubiconImpExtRpTrack.of("", ""), rubiconImpExtRpRtb); @@ -689,28 +704,20 @@ private RubiconImpExt makeImpExt(Imp imp, .build(); } - private ExtImpContext extImpContext(Imp imp) { - final JsonNode context = imp.getExt().get(FPD_CONTEXT_FIELD); - if (context == null || context.isNull()) { - return null; - } - try { - return mapper.mapper().convertValue(context, ExtImpContext.class); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); - } - } - - private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App app, ExtImpContext context) { + private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App app) { final ObjectNode result = mapper.mapper().createObjectNode(); populateFirstPartyDataAttributes(rubiconImpExt.getInventory(), result); mergeFirstPartyDataFromSite(site, result); mergeFirstPartyDataFromApp(app, result); - mergeFirstPartyDataFromImp(imp, rubiconImpExt, context, result); + mergeFirstPartyDataFromImp(imp, rubiconImpExt, result); - return result.size() > 0 ? result : null; + result.put(PBS_LOGIN, xapiUsername); + result.put(PBS_VERSION, versionProvider.getNameVersionRecord()); + result.put(PBS_URL, externalUrl); + + return result; } private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult, @@ -744,16 +751,8 @@ private void mergeFirstPartyDataFromSite(Site site, ObjectNode result) { populateFirstPartyDataAttributes(siteExt.getData(), result); } - // merge OPENRTB.site.sectioncat to every impression XAPI.imp[].ext.rp.target.sectioncat - mergeCollectionAttributeIntoArray(result, site, Site::getSectioncat, FPD_SECTIONCAT_FIELD); - // merge OPENRTB.site.pagecat to every impression XAPI.imp[].ext.rp.target.pagecat - mergeCollectionAttributeIntoArray(result, site, Site::getPagecat, FPD_PAGECAT_FIELD); // merge OPENRTB.site.page to every impression XAPI.imp[].ext.rp.target.page mergeStringAttributeIntoArray(result, site, Site::getPage, FPD_PAGE_FIELD); - // merge OPENRTB.site.ref to every impression XAPI.imp[].ext.rp.target.ref - mergeStringAttributeIntoArray(result, site, Site::getRef, FPD_REF_FIELD); - // merge OPENRTB.site.search to every impression XAPI.imp[].ext.rp.target.search - mergeStringAttributeIntoArray(result, site, Site::getSearch, FPD_SEARCH_FIELD); } private void mergeFirstPartyDataFromApp(App app, ObjectNode result) { @@ -762,72 +761,43 @@ private void mergeFirstPartyDataFromApp(App app, ObjectNode result) { if (appExt != null) { populateFirstPartyDataAttributes(appExt.getData(), result); } - - // merge OPENRTB.app.sectioncat to every impression XAPI.imp[].ext.rp.target.sectioncat - mergeCollectionAttributeIntoArray(result, app, App::getSectioncat, FPD_SECTIONCAT_FIELD); - // merge OPENRTB.app.pagecat to every impression XAPI.imp[].ext.rp.target.pagecat - mergeCollectionAttributeIntoArray(result, app, App::getPagecat, FPD_PAGECAT_FIELD); } private void mergeFirstPartyDataFromImp(Imp imp, ExtImpRubicon rubiconImpExt, - ExtImpContext context, ObjectNode result) { - mergeFirstPartyDataFromData(imp, context, result); - mergeFirstPartyDataKeywords(imp, context, result); + mergeFirstPartyDataFromData(imp, result); + mergeFirstPartyDataKeywords(imp, result); // merge OPENRTB.imp[].ext.rubicon.keywords to XAPI.imp[].ext.rp.target.keywords mergeCollectionAttributeIntoArray(result, rubiconImpExt, ExtImpRubicon::getKeywords, FPD_KEYWORDS_FIELD); - // merge OPENRTB.imp[].ext.context.search to XAPI.imp[].ext.rp.target.search - mergeStringAttributeIntoArray( - result, - context, - extContext -> getTextValueFromNode(extContext.getProperty(FPD_SEARCH_FIELD)), - FPD_SEARCH_FIELD); - // merge OPENRTB.imp[].ext.data.search to XAPI.imp[].ext.rp.target.search - mergeStringAttributeIntoArray( - result, - imp.getExt().get(FPD_DATA_FIELD), - node -> getTextValueFromNodeByPath(node, FPD_SEARCH_FIELD), - FPD_SEARCH_FIELD); - } - - private void mergeFirstPartyDataFromData(Imp imp, ExtImpContext context, ObjectNode result) { - final ObjectNode contextDataNode = toObjectNode( - ObjectUtil.getIfNotNull(context, ExtImpContext::getData)); - // merge OPENRTB.imp[].ext.context.data.* to XAPI.imp[].ext.rp.target.* - populateFirstPartyDataAttributes(contextDataNode, result); + } + private void mergeFirstPartyDataFromData(Imp imp, ObjectNode result) { final ObjectNode dataNode = toObjectNode(imp.getExt().get(FPD_DATA_FIELD)); // merge OPENRTB.imp[].ext.data.* to XAPI.imp[].ext.rp.target.* populateFirstPartyDataAttributes(dataNode, result); // override XAPI.imp[].ext.rp.target.* with OPENRTB.imp[].ext.data.* - overrideFirstPartyDataAttributes(contextDataNode, dataNode, result); + overrideFirstPartyDataAttributes(dataNode, result); } - private void overrideFirstPartyDataAttributes(ObjectNode contextDataNode, ObjectNode dataNode, ObjectNode result) { + private void overrideFirstPartyDataAttributes(ObjectNode dataNode, ObjectNode result) { final JsonNode pbadslotNode = dataNode.get(FPD_DATA_PBADSLOT_FIELD); if (pbadslotNode != null && pbadslotNode.isTextual()) { // copy imp[].ext.data.pbadslot to XAPI.imp[].ext.rp.target.pbadslot result.set(FPD_DATA_PBADSLOT_FIELD, pbadslotNode); } else { // copy adserver.adslot value to XAPI field imp[].ext.rp.target.dfp_ad_unit_code - final String resolvedDfpAdUnitCode = getAdSlot(contextDataNode, dataNode); + final String resolvedDfpAdUnitCode = getAdSlotFromAdServer(dataNode); if (resolvedDfpAdUnitCode != null) { result.set(DFP_ADUNIT_CODE_FIELD, TextNode.valueOf(resolvedDfpAdUnitCode)); } } - } - private void mergeFirstPartyDataKeywords(Imp imp, ExtImpContext context, ObjectNode result) { - // merge OPENRTB.imp[].ext.context.keywords to XAPI.imp[].ext.rp.target.keywords - final JsonNode keywordsNode = context != null ? context.getProperty("keywords") : null; - final String keywords = getTextValueFromNode(keywordsNode); - if (StringUtils.isNotBlank(keywords)) { - mergeIntoArray(result, FPD_KEYWORDS_FIELD, keywords.split(",")); - } + } + private void mergeFirstPartyDataKeywords(Imp imp, ObjectNode result) { // merge OPENRTB.imp[].ext.data.keywords to XAPI.imp[].ext.rp.target.keywords final String dataKeywords = getTextValueFromNodeByPath(imp.getExt().get(FPD_DATA_FIELD), FPD_KEYWORDS_FIELD); if (StringUtils.isNotBlank(dataKeywords)) { @@ -961,7 +931,7 @@ private Integer getMaxBids(ExtRequest extRequest) { final List multibids = extRequestPrebid != null ? extRequestPrebid.getMultibid() : null; final ExtRequestPrebidMultiBid extRequestPrebidMultiBid = - CollectionUtils.isNotEmpty(multibids) ? multibids.get(0) : null; + CollectionUtils.isNotEmpty(multibids) ? multibids.getFirst() : null; final Integer multibidMaxBids = extRequestPrebidMultiBid != null ? extRequestPrebidMultiBid.getMaxBids() : null; return multibidMaxBids != null ? multibidMaxBids : 1; @@ -977,19 +947,10 @@ private ObjectNode getSkadn(ObjectNode impExt) { return skadnNode != null && skadnNode.isObject() ? (ObjectNode) skadnNode : null; } - private String getAdSlot(Imp imp, ExtImpContext context) { - final ObjectNode contextDataNode = context != null ? context.getData() : null; + private String getAdSlot(Imp imp) { final ObjectNode dataNode = toObjectNode(imp.getExt().get(FPD_DATA_FIELD)); - return getAdSlot(contextDataNode, dataNode); - } - - private String getAdSlot(ObjectNode contextDataNode, ObjectNode dataNode) { - return ObjectUtils.firstNonNull( - // or imp[].ext.context.data.adserver.adslot - getAdSlotFromAdServer(contextDataNode), - // or imp[].ext.data.adserver.adslot - getAdSlotFromAdServer(dataNode)); + return getAdSlotFromAdServer(dataNode); } private String getAdSlotFromAdServer(JsonNode dataNode) { @@ -1042,12 +1003,7 @@ private Video makeVideo(Imp imp, RubiconVideoParams rubiconVideoParams, String r final Integer skip = rubiconVideoParams != null ? rubiconVideoParams.getSkip() : null; final Integer skipDelay = rubiconVideoParams != null ? rubiconVideoParams.getSkipdelay() : null; - final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; - - final Integer resolvedSizeId = BidderUtil.isNullOrZero(sizeId) - ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) - : sizeId; - validateVideoSizeId(resolvedSizeId, referer, imp.getId()); + final Integer resolvedSizeId = resolveSizeId(rubiconVideoParams, imp, referer); final Integer rewarded = imp.getRwdd(); final String videoType = rewarded != null && rewarded == 1 ? "rewarded" : null; @@ -1059,10 +1015,23 @@ private Video makeVideo(Imp imp, RubiconVideoParams rubiconVideoParams, String r return video.toBuilder() .ext(mapper.mapper().valueToTree( - RubiconVideoExt.of(skip, skipDelay, RubiconVideoExtRp.of(resolvedSizeId), videoType))) + RubiconVideoExt.of(skip, + skipDelay, + resolvedSizeId != null ? RubiconVideoExtRp.of(resolvedSizeId) : null, + videoType))) .build(); } + private Integer resolveSizeId(RubiconVideoParams rubiconVideoParams, Imp imp, String referer) { + final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; + final Integer resolvedSizeId = BidderUtil.isNullOrZero(sizeId) + ? null + : sizeId; + validateVideoSizeId(resolvedSizeId, referer, imp.getId()); + + return resolvedSizeId; + } + private static void validateVideoSizeId(Integer resolvedSizeId, String referer, String impId) { // log only 1% of cases to monitor how often video impressions does not have size id if (resolvedSizeId == null) { @@ -1073,23 +1042,6 @@ private static void validateVideoSizeId(Integer resolvedSizeId, String referer, } } - private static Integer resolveVideoSizeId(Integer placement, Integer instl) { - if (placement != null) { - if (placement == 1) { - return 201; - } - if (placement == 3) { - return 203; - } - } - - if (instl != null && instl == 1) { - return 202; - } - - return null; - } - private Banner makeBanner(Imp imp) { final Banner banner = imp.getBanner(); final Integer width = banner.getW(); @@ -1164,8 +1116,6 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { final String userId = user != null ? user.getId() : null; final List userEids = user != null ? user.getEids() : null; final String resolvedId = userId == null ? resolveUserId(userEids) : null; - final String userBuyeruid = user != null ? user.getBuyeruid() : null; - final String resolvedBuyeruid = userBuyeruid != null ? userBuyeruid : resolveBuyeruidFromEids(userEids); final ExtUser extUser = user != null ? user.getExt() : null; final boolean hasStypeToRemove = hasStypeToRemove(userEids); final List resolvedUserEids = hasStypeToRemove @@ -1179,9 +1129,7 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { && userExtData == null && resolvedUserEids == null && resolvedId == null - && Objects.equals(userBuyeruid, resolvedBuyeruid) - && !hasStypeToRemove - ) { + && !hasStypeToRemove) { return hasDataToRemove ? user.toBuilder().data(null).build() @@ -1200,7 +1148,6 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { return userBuilder .id(ObjectUtils.defaultIfNull(resolvedId, userId)) - .buyeruid(resolvedBuyeruid) .gender(null) .yob(null) .geo(null) @@ -1283,7 +1230,7 @@ private static Eid prepareExtUserEid(Eid extUserEid) { .filter(Objects::nonNull) .map(RubiconBidder::cleanExtUserEidUidStype) .toList(); - return Eid.of(extUserEid.getSource(), extUserEidUids, extUserEid.getExt()); + return extUserEid.toBuilder().uids(extUserEidUids).build(); } private static Uid cleanExtUserEidUidStype(Uid extUserEidUid) { @@ -1295,24 +1242,7 @@ private static Uid cleanExtUserEidUidStype(Uid extUserEidUid) { final ObjectNode extUserEidUidExtCopy = extUserEidUidExt.deepCopy(); extUserEidUidExtCopy.remove(STYPE_FIELD); - return Uid.of( - extUserEidUid.getId(), - extUserEidUid.getAtype(), - extUserEidUidExtCopy); - } - - private static String resolveBuyeruidFromEids(List eids) { - return CollectionUtils.emptyIfNull(eids).stream() - .filter(Objects::nonNull) - .filter(eid -> SOURCE_RUBICON.equals(eid.getSource())) - .map(Eid::getUids) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .map(Uid::getId) - .findFirst() - .orElse(null); - + return extUserEidUid.toBuilder().ext(extUserEidUidExtCopy).build(); } private RubiconUserExtRp rubiconUserExtRp(User user, ExtImpRubicon rubiconImpExt) { @@ -1329,7 +1259,7 @@ private JsonNode rubiconUserExtRpTarget(ObjectNode visitor, User user) { if (user != null) { mergeFirstPartyDataFromUser(user.getExt(), result); - enrichWithIabAttribute(result, user.getData(), USER_SEGTAXES); + enrichWithIabAndSegtaxAttribute(result, user.getData(), USER_SEGTAXES); } return !result.isEmpty() ? result : null; @@ -1353,27 +1283,97 @@ private void mergeFirstPartyDataFromUser(ExtUser userExt, ObjectNode result) { } } - private static void enrichWithIabAttribute(ObjectNode target, List data, Set segtaxValues) { - final List iabValue = CollectionUtils.emptyIfNull(data).stream() - .filter(Objects::nonNull) - .filter(dataRecord -> containsSegtaxValue(dataRecord.getExt(), segtaxValues)) - .map(Data::getSegment) + private static void enrichWithIabAndSegtaxAttribute(ObjectNode target, List data, Set segtaxValues) { + final Map> segments = CollectionUtils.emptyIfNull(data).stream() .filter(Objects::nonNull) - .flatMap(segments -> segments.stream() - .map(Segment::getId)) + .map(RubiconBidder::getValidSegments) .filter(Objects::nonNull) - .toList(); + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (first, second) -> { + first.addAll(second); + return first; + })); + + if (MapUtils.isEmpty(segments)) { + return; + } + + final Map> relevantSegments = pickRelevantSegments(segments); + final Map> resultSegments = groupBySegtaxValues(relevantSegments, segtaxValues); + + resultSegments.forEach((segtaxValue, segmentIds) -> { + final ArrayNode array = target.putArray(segtaxValue); + segmentIds.forEach(array::add); + }); + } - if (CollectionUtils.isNotEmpty(iabValue)) { - final ArrayNode iab = target.putArray("iab"); - iabValue.forEach(iab::add); + private static Map> groupBySegtaxValues(Map> segments, + Set segtaxValues) { + + return segments.entrySet().stream() + .collect(Collectors.toMap( + entry -> resolveSegmentName(entry.getKey(), segtaxValues), + Map.Entry::getValue, + ListUtil::union)); + } + + private static String resolveSegmentName(Integer taxonomyId, Set segtaxValues) { + return segtaxValues.contains(taxonomyId) ? SEGTAX_IAB : SEGTAX_TAX + taxonomyId; + } + + private static Map.Entry> getValidSegments(Data data) { + final ObjectNode ext = data.getExt(); + final JsonNode taxonomyId = ext != null ? ext.get(SEGTAX) : null; + if (taxonomyId == null || !taxonomyId.isInt()) { + return null; } + + final Deque segments = getValidOnlySegments(data.getSegment()); + return CollectionUtils.isNotEmpty(segments) ? Map.entry(taxonomyId.intValue(), segments) : null; + } + + private static Deque getValidOnlySegments(List segments) { + return CollectionUtils.isNotEmpty(segments) + ? segments.stream() + .filter(segment -> StringUtils.isNotBlank(segment.getId())) + .collect(Collectors.toCollection(ArrayDeque::new)) + : null; + } + + private static Map> pickRelevantSegments(final Map> segments) { + final Map> result = new HashMap<>(); + final List segmentsKeys = new ArrayList<>(segments.keySet()); + + int i = 0; + int consumedSegmentsCount = 0; + + while (consumedSegmentsCount < MAX_NUMBER_OF_SEGMENTS && !segmentsKeys.isEmpty()) { + final int segmentsIndex = i % segmentsKeys.size(); + final Integer segmentKey = segmentsKeys.get(segmentsIndex); + final Deque currentSegments = segments.get(segmentKey); + + final Segment lastSegment = currentSegments.pollLast(); + result.computeIfAbsent(segmentKey, key -> new ArrayList<>()).add(lastSegment.getId()); + consumedSegmentsCount++; + + if (currentSegments.isEmpty()) { + segmentsKeys.remove(segmentKey); + i--; + } + i++; + } + + return result; } - private static boolean containsSegtaxValue(ObjectNode ext, Set segtaxValues) { - final JsonNode taxonomyName = ext != null ? ext.get("segtax") : null; + private static Segment getAndRemoveLastSegment(List list) { + final int lastElementIndex = list.size() - 1; + final Segment lastSegment = list.get(lastElementIndex); + list.remove(lastElementIndex); - return taxonomyName != null && taxonomyName.isInt() && segtaxValues.contains(taxonomyName.intValue()); + return lastSegment; } private void processWarnings(List errors, List priceFloorsWarnings) { @@ -1445,7 +1445,7 @@ private ExtSite makeSiteExt(Site site, ExtImpRubicon rubiconImpExt) { if (CollectionUtils.isNotEmpty(siteContentData)) { target = existingRubiconSiteExtRpTargetOrEmptyNode(extSite); - enrichWithIabAttribute(target, siteContentData, SITE_SEGTAXES); + enrichWithIabAndSegtaxAttribute(target, siteContentData, SITE_SEGTAXES); } return mapper.fillExtension( @@ -1476,21 +1476,16 @@ private ExtApp makeAppExt(ExtImpRubicon rubiconImpExt) { RubiconAppExt.of(RubiconSiteExtRp.of(rubiconImpExt.getSiteId(), null))); } - private static Source makeSource(Source source, String pchain) { - final boolean isPchainEmpty = StringUtils.isEmpty(pchain); + private static Source makeSource(Source source) { final SupplyChain supplyChain = source != null ? source.getSchain() : null; - if (isPchainEmpty && supplyChain == null) { + if (supplyChain == null) { return source; } - final ExtSource extSource = source != null ? source.getExt() : null; - final ExtSource resolvedExtSource = supplyChain != null - ? copyProperties(extSource, ExtSource.of(supplyChain)) - : extSource; + final ExtSource extSource = source.getExt(); + final ExtSource resolvedExtSource = copyProperties(extSource, ExtSource.of(supplyChain)); - final Source.SourceBuilder builder = source != null ? source.toBuilder() : Source.builder(); - return builder - .pchain(!isPchainEmpty ? pchain : null) + return source.toBuilder() .schain(null) .ext(resolvedExtSource) .build(); @@ -1543,7 +1538,7 @@ private static boolean hasDeals(Imp imp) { } private List> createDealsRequests(BidRequest bidRequest, String uri) { - final Imp singleImp = bidRequest.getImp().get(0); + final Imp singleImp = bidRequest.getImp().getFirst(); return singleImp.getPmp().getDeals().stream() .map(deal -> mapper.mapper().convertValue(deal.getExt(), ExtDeal.class)) .filter(Objects::nonNull) @@ -1558,7 +1553,7 @@ private BidRequest createLineItemBidRequest(ExtDealLine lineItem, BidRequest bid final Imp dealsImp = imp.toBuilder() .banner(modifyBanner(imp.getBanner(), lineItem.getSizes())) .ext(modifyRubiconImpExt(imp.getExt(), bidRequest.getExt(), lineItem.getExtLineItemId(), - getAdSlot(imp, extImpContext(imp)))) + getAdSlot(imp))) .build(); return bidRequest.toBuilder() @@ -1735,7 +1730,7 @@ private Float cpmOverrideFromImp(Imp imp) { } private static BidType bidType(BidRequest bidRequest) { - final ImpMediaType impMediaType = impType(bidRequest.getImp().get(0)); + final ImpMediaType impMediaType = impType(bidRequest.getImp().getFirst()); return switch (impMediaType) { case video -> BidType.video; case banner -> BidType.banner; diff --git a/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java b/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java index e0e1c8cc6af..0dcdd206050 100644 --- a/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java @@ -54,13 +54,13 @@ private List extractBids(BidResponse bidResponse) { throw new PreBidException("Empty SeatBid"); } - final SeatBid firstSeatBid = seatBids.get(0); + final SeatBid firstSeatBid = seatBids.getFirst(); final List bids = firstSeatBid != null ? firstSeatBid.getBid() : null; if (CollectionUtils.isEmpty(bids)) { throw new PreBidException("Empty SeatBid.Bids"); } - final Bid firstBid = bids.get(0); + final Bid firstBid = bids.getFirst(); final ObjectNode firstBidExt = firstBid != null ? firstBid.getExt() : null; if (firstBidExt == null) { throw new PreBidException("Missing BidExt"); diff --git a/src/main/java/org/prebid/server/bidder/screencore/ScreencoreBidder.java b/src/main/java/org/prebid/server/bidder/screencore/ScreencoreBidder.java index 9a6a6ba24ce..51e05ddfd24 100644 --- a/src/main/java/org/prebid/server/bidder/screencore/ScreencoreBidder.java +++ b/src/main/java/org/prebid/server/bidder/screencore/ScreencoreBidder.java @@ -48,7 +48,7 @@ public ScreencoreBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final ScreencoreImpExt impExt; - final Imp firstImp = request.getImp().get(0); + final Imp firstImp = request.getImp().getFirst(); try { impExt = parseImpExt(firstImp); } catch (PreBidException e) { @@ -84,7 +84,7 @@ private ScreencoreImpExt parseImpExt(Imp imp) { private static BidRequest cleanUpFirstImpExt(BidRequest request) { final List imps = new ArrayList<>(request.getImp()); - imps.set(0, request.getImp().get(0).toBuilder().ext(null).build()); + imps.set(0, request.getImp().getFirst().toBuilder().ext(null).build()); return request.toBuilder().imp(imps).build(); } diff --git a/src/main/java/org/prebid/server/bidder/seedingAlliance/SeedingAllianceBidder.java b/src/main/java/org/prebid/server/bidder/seedingAlliance/SeedingAllianceBidder.java index 93a8a95a891..d6e8a880778 100644 --- a/src/main/java/org/prebid/server/bidder/seedingAlliance/SeedingAllianceBidder.java +++ b/src/main/java/org/prebid/server/bidder/seedingAlliance/SeedingAllianceBidder.java @@ -41,7 +41,7 @@ public class SeedingAllianceBidder implements Bidder { private static final String EUR_CURRENCY = "EUR"; private static final String AUCTION_PRICE_MACRO = "${AUCTION_PRICE}"; private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; - private static final String DEFAULT_SEAT_ID = "pbs"; + private static final String DEFAULT_ACCOUNT_ID = "pbs"; private final String endpointUrl; private final JacksonMapper mapper; @@ -53,12 +53,14 @@ public SeedingAllianceBidder(String endpointUrl, JacksonMapper mapper) { @Override public final Result>> makeHttpRequests(BidRequest bidRequest) { - String seatId = null; + String accountId = null; final List modifiedImps = new ArrayList<>(); for (Imp imp: bidRequest.getImp()) { try { final ExtImpSeedingAlliance impExt = parseImpExt(imp); - seatId = impExt.getSeatId(); + accountId = StringUtils.isNotBlank(impExt.getAccountId()) + ? impExt.getAccountId() + : StringUtils.isNotBlank(impExt.getSeatId()) ? impExt.getSeatId() : null; final Imp modifiedImp = imp.toBuilder().tagid(impExt.getAdUnitId()).build(); modifiedImps.add(modifiedImp); } catch (PreBidException e) { @@ -67,7 +69,7 @@ public final Result>> makeHttpRequests(BidRequest b } final BidRequest modifiedBidRequest = modifyBidRequest(bidRequest, modifiedImps); - return Result.withValue(makeHttpRequest(seatId, modifiedBidRequest)); + return Result.withValue(makeHttpRequest(accountId, modifiedBidRequest)); } private ExtImpSeedingAlliance parseImpExt(Imp imp) { @@ -98,10 +100,10 @@ private static List modifyCurrencies(List bidderCurrencies) { return Collections.unmodifiableList(resolvedCurrencies); } - private HttpRequest makeHttpRequest(String seatId, BidRequest modifiedBidRequest) { + private HttpRequest makeHttpRequest(String accountId, BidRequest modifiedBidRequest) { return HttpRequest.builder() .method(HttpMethod.POST) - .uri(makeEndpoint(seatId)) + .uri(makeEndpoint(accountId)) .headers(HttpUtil.headers()) .body(mapper.encodeToBytes(modifiedBidRequest)) .impIds(BidderUtil.impIds(modifiedBidRequest)) @@ -109,9 +111,9 @@ private HttpRequest makeHttpRequest(String seatId, BidRequest modifi .build(); } - private String makeEndpoint(String seatId) { - final String accountId = StringUtils.isNotBlank(seatId) ? seatId : DEFAULT_SEAT_ID; - return endpointUrl.replace(ACCOUNT_ID_MACRO, accountId); + private String makeEndpoint(String accountId) { + final String marcoReplacement = StringUtils.isNotBlank(accountId) ? accountId : DEFAULT_ACCOUNT_ID; + return endpointUrl.replace(ACCOUNT_ID_MACRO, marcoReplacement); } @Override diff --git a/src/main/java/org/prebid/server/bidder/silverpush/SilverPushBidder.java b/src/main/java/org/prebid/server/bidder/silverpush/SilverPushBidder.java index cfa91c1df5b..212e1558283 100644 --- a/src/main/java/org/prebid/server/bidder/silverpush/SilverPushBidder.java +++ b/src/main/java/org/prebid/server/bidder/silverpush/SilverPushBidder.java @@ -154,7 +154,7 @@ private static boolean isValidEids(List eids) { } for (Eid eid : eids) { final List uids = eid.getUids(); - if (CollectionUtils.isNotEmpty(uids) && StringUtils.isNotBlank(uids.get(0).getId())) { + if (CollectionUtils.isNotEmpty(uids) && StringUtils.isNotBlank(uids.getFirst().getId())) { return true; } } @@ -230,7 +230,7 @@ private static Banner resolveBanner(Banner banner) { throw new PreBidException("No sizes provided for Banner."); } - final Format firstFormat = banner.getFormat().get(0); + final Format firstFormat = banner.getFormat().getFirst(); return banner.toBuilder() .w(firstFormat.getW()) .h(firstFormat.getH()) diff --git a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java index 1c62d283687..414dc9844c3 100644 --- a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java +++ b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java @@ -6,7 +6,7 @@ import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Publisher; @@ -29,16 +29,12 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.smaato.proto.SmaatoBidExt; import org.prebid.server.bidder.smaato.proto.SmaatoBidRequestExt; -import org.prebid.server.bidder.smaato.proto.SmaatoImage; -import org.prebid.server.bidder.smaato.proto.SmaatoImageAd; -import org.prebid.server.bidder.smaato.proto.SmaatoImg; -import org.prebid.server.bidder.smaato.proto.SmaatoMediaData; -import org.prebid.server.bidder.smaato.proto.SmaatoRichMediaAd; -import org.prebid.server.bidder.smaato.proto.SmaatoRichmedia; +import org.prebid.server.bidder.smaato.proto.SmaatoNativeAd; import org.prebid.server.bidder.smaato.proto.SmaatoSiteExtData; import org.prebid.server.bidder.smaato.proto.SmaatoUserExtData; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; +import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -48,7 +44,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.smaato.ExtImpSmaato; import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -69,12 +64,13 @@ public class SmaatoBidder implements Bidder { private static final TypeReference> SMAATO_EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String CLIENT_VERSION = "prebid_server_0.4"; + private static final String CLIENT_VERSION = "prebid_server_1.1"; private static final String SMT_ADTYPE_HEADER = "X-Smt-Adtype"; private static final String SMT_EXPIRES_HEADER = "X-Smt-Expires"; private static final String SMT_AD_TYPE_IMG = "Img"; private static final String SMT_ADTYPE_RICHMEDIA = "Richmedia"; private static final String SMT_ADTYPE_VIDEO = "Video"; + private static final String SMT_ADTYPE_NATIVE = "Native"; private static final String IMP_EXT_SKADN_FIELD = "skadn"; private static final int DEFAULT_TTL = 300; @@ -148,11 +144,14 @@ private User modifyUser(User user) { } private Site modifySite(Site site) { + if (site == null) { + return null; + } final ExtSite siteExt = getIfNotNull(site, Site::getExt); if (siteExt != null) { final SmaatoSiteExtData data = convertExt(siteExt.getData(), SmaatoSiteExtData.class); final String keywords = getIfNotNull(data, SmaatoSiteExtData::getKeywords); - return Site.builder().keywords(keywords).ext(null).build(); + return site.toBuilder().keywords(keywords).ext(null).build(); } return site; } @@ -198,7 +197,7 @@ private static String extractPod(Imp imp) { private BidRequest preparePodRequest(BidRequest bidRequest, List imps, List errors) { try { - final ObjectNode impExt = imps.get(0).getExt(); + final ObjectNode impExt = imps.getFirst().getExt(); final ExtImpSmaato extImpSmaato = mapper.mapper().convertValue(impExt, SMAATO_EXT_TYPE_REFERENCE).getBidder(); final String publisherId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getPublisherId, "publisherId"); @@ -216,14 +215,17 @@ private BidRequest modifyBidRequest(BidRequest bidRequest, String publisherId, S final Publisher publisher = Publisher.builder().id(publisherId).build(); final Site site = bidRequest.getSite(); final App app = bidRequest.getApp(); + final Dooh dooh = bidRequest.getDooh(); final BidRequest.BidRequestBuilder bidRequestBuilder = bidRequest.toBuilder(); if (site != null) { bidRequestBuilder.site(site.toBuilder().publisher(publisher).build()); } else if (app != null) { bidRequestBuilder.app(app.toBuilder().publisher(publisher).build()); + } else if (dooh != null) { + bidRequestBuilder.dooh(dooh.toBuilder().publisher(publisher).build()); } else { - throw new PreBidException("Missing Site/App."); + throw new PreBidException("Missing Site/App/DOOH."); } return bidRequestBuilder.imp(impSupplier.get()).build(); @@ -311,25 +313,12 @@ private ObjectNode resolveImpExtSkadn(ObjectNode impExt) { private List modifyImpForAdSpace(Imp imp, String adSpaceId, ObjectNode impExtSkadn) { final Imp modifiedImp = imp.toBuilder() .tagid(adSpaceId) - .banner(getIfNotNull(imp.getBanner(), SmaatoBidder::modifyBanner)) .ext(impExtSkadn) .build(); return Collections.singletonList(modifiedImp); } - private static Banner modifyBanner(Banner banner) { - if (banner.getW() != null && banner.getH() != null) { - return banner; - } - final List format = banner.getFormat(); - if (CollectionUtils.isEmpty(format)) { - throw new PreBidException("No sizes provided for Banner."); - } - final Format firstFormat = format.get(0); - return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); - } - private HttpRequest constructHttpRequest(BidRequest bidRequest) { return BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper); } @@ -349,56 +338,66 @@ private Result> extractBids(BidResponse bidResponse, MultiMap he return Result.empty(); } + final String markupType = getAdMarkupType(headers); final List errors = new ArrayList<>(); final List bidderBids = bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> bidderBid(bid, bidResponse.getCur(), headers, errors)) + .map(bid -> bidderBid(bid, bidResponse.getCur(), markupType, headers, errors)) .filter(Objects::nonNull) .toList(); return Result.of(bidderBids, errors); } - private BidderBid bidderBid(Bid bid, String currency, MultiMap headers, List errors) { + private BidderBid bidderBid(Bid bid, + String currency, + String markupType, + MultiMap headers, + List errors) { try { final String bidAdm = bid.getAdm(); if (StringUtils.isBlank(bidAdm)) { throw new PreBidException("Empty ad markup in bid with id: " + bid.getId()); } - final String markupType = getAdMarkupType(headers, bidAdm); + final SmaatoBidExt bidExt = parseBidExt(bid.getExt()); final BidType bidType = getBidType(markupType); final Bid updatedBid = bid.toBuilder() - .adm(renderAdMarkup(markupType, bidAdm)) + .adm(renderAdMarkup(markupType, bidAdm, bidExt)) .exp(getTtl(headers)) - .ext(buildExtPrebid(bid, bidType)) .build(); - return BidderBid.of(updatedBid, bidType, currency); + return BidderBid.builder() + .bid(updatedBid) + .type(bidType) + .bidCurrency(currency) + .videoInfo(getExtBidPrebidVideo(bid, bidType, bidExt)) + .build(); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); return null; } } - private ObjectNode buildExtPrebid(Bid bid, BidType bidType) { - final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid, bidType); - final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().video(extBidPrebidVideo).build(); - return mapper.mapper().valueToTree(ExtPrebid.of(extBidPrebid, null)); - } - - private ExtBidPrebidVideo getExtBidPrebidVideo(Bid bid, BidType bidType) { - final ObjectNode bidExt = bid.getExt(); - if (bidType != BidType.video || bidExt == null) { + private ExtBidPrebidVideo getExtBidPrebidVideo(Bid bid, BidType bidType, SmaatoBidExt bidExt) { + if (bidType != BidType.video) { return null; } final List categories = bid.getCat(); - final String primaryCategory = CollectionUtils.isNotEmpty(categories) ? categories.get(0) : null; + final String primaryCategory = CollectionUtils.isNotEmpty(categories) ? categories.getFirst() : null; try { - final SmaatoBidExt smaatoBidExt = mapper.mapper().convertValue(bidExt, SmaatoBidExt.class); - return ExtBidPrebidVideo.of(smaatoBidExt.getDuration(), primaryCategory); + return ExtBidPrebidVideo.of(bidExt.getDuration(), primaryCategory); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid bid.ext."); + } + } + + private SmaatoBidExt parseBidExt(ObjectNode bidExt) { + try { + final SmaatoBidExt parsedExt = mapper.mapper().convertValue(bidExt, SmaatoBidExt.class); + return parsedExt == null ? SmaatoBidExt.empty() : parsedExt; } catch (IllegalArgumentException e) { throw new PreBidException("Invalid bid.ext."); } @@ -414,86 +413,41 @@ private int getTtl(MultiMap headers) { } } - private static String getAdMarkupType(MultiMap headers, String adm) { + private static String getAdMarkupType(MultiMap headers) { final String adMarkupType = headers.get(SMT_ADTYPE_HEADER); if (StringUtils.isNotBlank(adMarkupType)) { return adMarkupType; - } else if (adm.startsWith("{\"image\":")) { - return SMT_AD_TYPE_IMG; - } else if (adm.startsWith("{\"richmedia\":")) { - return SMT_ADTYPE_RICHMEDIA; - } else if (adm.startsWith(" extractAdmImage(adm); - case SMT_ADTYPE_RICHMEDIA -> extractAdmRichMedia(adm); - case SMT_ADTYPE_VIDEO -> markupType; + case SMT_AD_TYPE_IMG, SMT_ADTYPE_RICHMEDIA -> extractAdmBanner(adm, bidExt.getCurls()); + case SMT_ADTYPE_VIDEO -> adm; + case SMT_ADTYPE_NATIVE -> extractNative(adm); default -> throw new PreBidException("Unknown markup type " + markupType); }; } - private String extractAdmImage(String adm) { - final SmaatoImageAd imageAd = convertAdmToAd(adm, SmaatoImageAd.class); - final SmaatoImage image = imageAd.getImage(); - if (image == null) { - throw new PreBidException("bid.adm.image is empty"); - } - - final StringBuilder clickEvent = new StringBuilder(); - CollectionUtils.emptyIfNull(image.getClickTrackers()) - .forEach(tracker -> clickEvent.append( - "fetch(decodeURIComponent('%s'.replace(/\\+/g, ' ')), {cache: 'no-cache'});" - .formatted(HttpUtil.encodeUrl(StringUtils.stripToEmpty(tracker))))); - - final StringBuilder impressionTracker = new StringBuilder(); - CollectionUtils.emptyIfNull(image.getImpressionTrackers()) - .forEach(tracker -> impressionTracker.append( - "\"\"".formatted(tracker))); - - final SmaatoImg img = image.getImg(); - return """ -

%s
""".formatted( - clickEvent, - HttpUtil.encodeUrl(StringUtils.stripToEmpty(getIfNotNull(img, SmaatoImg::getCtaurl))), - StringUtils.stripToEmpty(getIfNotNull(img, SmaatoImg::getUrl)), - stripToZero(getIfNotNull(img, SmaatoImg::getW)), - stripToZero(getIfNotNull(img, SmaatoImg::getH)), - impressionTracker); - } - - private String extractAdmRichMedia(String adm) { - final SmaatoRichMediaAd richMediaAd = convertAdmToAd(adm, SmaatoRichMediaAd.class); - final SmaatoRichmedia richmedia = richMediaAd.getRichmedia(); - if (richmedia == null) { - throw new PreBidException("bid.adm.richmedia is empty"); + private String extractAdmBanner(String adm, List curls) { + if (CollectionUtils.isEmpty(curls)) { + return adm; } final StringBuilder clickEvent = new StringBuilder(); - CollectionUtils.emptyIfNull(richmedia.getClickTrackers()) - .forEach(tracker -> clickEvent.append("fetch(decodeURIComponent('%s'), {cache: 'no-cache'});" - .formatted(HttpUtil.encodeUrl(StringUtils.stripToEmpty(tracker))))); - - final StringBuilder impressionTracker = new StringBuilder(); - CollectionUtils.emptyIfNull(richmedia.getImpressionTrackers()) - .forEach(tracker -> impressionTracker.append( - "\"\"".formatted(tracker))); + curls.forEach(url -> clickEvent.append( + "fetch(decodeURIComponent('%s'.replace(/\\+/g, ' ')), {cache: 'no-cache'});" + .formatted(HttpUtil.encodeUrl(StringUtils.stripToEmpty(url))))); - return "
%s%s
".formatted( - clickEvent, - StringUtils.stripToEmpty(getIfNotNull(richmedia.getMediadata(), SmaatoMediaData::getContent)), - impressionTracker); + return "
%s
".formatted(clickEvent, adm); } - private T convertAdmToAd(String value, Class className) { + private String extractNative(String adm) { try { - return mapper.decodeValue(value, className); - } catch (DecodeException e) { + final SmaatoNativeAd nativeAd = mapper.decodeValue(adm, SmaatoNativeAd.class); + return mapper.encodeToString(nativeAd.getNativeRequest()); + } catch (DecodeException | EncodeException e) { throw new PreBidException("Cannot decode bid.adm: " + e.getMessage(), e); } } @@ -502,6 +456,7 @@ private static BidType getBidType(String markupType) { return switch (markupType) { case SMT_AD_TYPE_IMG, SMT_ADTYPE_RICHMEDIA -> BidType.banner; case SMT_ADTYPE_VIDEO -> BidType.video; + case SMT_ADTYPE_NATIVE -> BidType.xNative; default -> throw new PreBidException("Invalid markupType " + markupType); }; } @@ -517,8 +472,4 @@ private static R getIfNotNullOrThrow(T target, Function getter, Str private static R getIfNotNull(T target, Function getter) { return target != null ? getter.apply(target) : null; } - - private static int stripToZero(Integer target) { - return ObjectUtils.defaultIfNull(target, 0); - } } diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java index b278d2a084e..755c598ec05 100644 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java +++ b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java @@ -1,11 +1,19 @@ package org.prebid.server.bidder.smaato.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +import java.util.List; + +@Value(staticConstructor = "of") public class SmaatoBidExt { + private static final SmaatoBidExt EMPTY = SmaatoBidExt.of(null, null); + Integer duration; + + List curls; + + public static SmaatoBidExt empty() { + return EMPTY; + } } diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidRequestExt.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidRequestExt.java index 7d50e5633b4..07dede0c71d 100644 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidRequestExt.java +++ b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidRequestExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.smaato.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class SmaatoBidRequestExt { String client; diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImage.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImage.java deleted file mode 100644 index 2c8ae9aadb4..00000000000 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImage.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.bidder.smaato.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class SmaatoImage { - - SmaatoImg img; - - @JsonProperty("impressiontrackers") - List impressionTrackers; - - @JsonProperty("clicktrackers") - List clickTrackers; -} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImageAd.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImageAd.java deleted file mode 100644 index 4e091e0188c..00000000000 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImageAd.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.smaato.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class SmaatoImageAd { - - SmaatoImage image; -} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImg.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImg.java deleted file mode 100644 index c5c8d616e0c..00000000000 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoImg.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.bidder.smaato.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class SmaatoImg { - - String url; - - Integer w; - - Integer h; - - String ctaurl; -} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoMediaData.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoMediaData.java deleted file mode 100644 index 32a7e127f0b..00000000000 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoMediaData.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.bidder.smaato.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class SmaatoMediaData { - - String content; - - Integer w; - - Integer h; -} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoNativeAd.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoNativeAd.java new file mode 100644 index 00000000000..dcdba99fe15 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoNativeAd.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.smaato.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SmaatoNativeAd { + + @JsonProperty("native") + ObjectNode nativeRequest; + +} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoRichMediaAd.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoRichMediaAd.java deleted file mode 100644 index ca8639e219d..00000000000 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoRichMediaAd.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.smaato.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class SmaatoRichMediaAd { - - SmaatoRichmedia richmedia; -} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoRichmedia.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoRichmedia.java deleted file mode 100644 index 825db41a1c0..00000000000 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoRichmedia.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.bidder.smaato.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class SmaatoRichmedia { - - SmaatoMediaData mediadata; - - @JsonProperty("impressiontrackers") - List impressionTrackers; - - @JsonProperty("clicktrackers") - List clickTrackers; -} diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoSiteExtData.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoSiteExtData.java index 2440df6d348..a7c69b6fc3c 100644 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoSiteExtData.java +++ b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoSiteExtData.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.smaato.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class SmaatoSiteExtData { String keywords; diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoUserExtData.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoUserExtData.java index ad8003d9e4f..acbd951ea05 100644 --- a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoUserExtData.java +++ b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoUserExtData.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.smaato.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class SmaatoUserExtData { String keywords; diff --git a/src/main/java/org/prebid/server/bidder/smarthub/SmarthubBidder.java b/src/main/java/org/prebid/server/bidder/smarthub/SmarthubBidder.java index 272480a7123..ae00fa4675e 100644 --- a/src/main/java/org/prebid/server/bidder/smarthub/SmarthubBidder.java +++ b/src/main/java/org/prebid/server/bidder/smarthub/SmarthubBidder.java @@ -45,7 +45,7 @@ public SmarthubBidder(String endpointTemplate, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final Imp firstImp = request.getImp().get(0); + final Imp firstImp = request.getImp().getFirst(); final ExtImpSmarthub extImpSmarthub; try { extImpSmarthub = mapper.mapper().convertValue(firstImp.getExt(), SMARTHUB_EXT_TYPE_REFERENCE).getBidder(); @@ -84,9 +84,9 @@ public Result> makeBids(BidderCall httpCall, BidRequ private List extractBids(BidResponse bidResponse) { final List seatBid = bidResponse != null ? bidResponse.getSeatbid() : null; - final SeatBid firstSeatBid = CollectionUtils.isNotEmpty(seatBid) ? seatBid.get(0) : null; + final SeatBid firstSeatBid = CollectionUtils.isNotEmpty(seatBid) ? seatBid.getFirst() : null; final List bids = firstSeatBid != null ? firstSeatBid.getBid() : null; - final Bid firstBid = CollectionUtils.isNotEmpty(bids) ? bids.get(0) : null; + final Bid firstBid = CollectionUtils.isNotEmpty(bids) ? bids.getFirst() : null; if (firstBid == null) { throw new PreBidException("SeatBid[0].Bid[0] cannot be empty"); diff --git a/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java index 1f5c17dde55..f605e564a49 100644 --- a/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java @@ -146,7 +146,7 @@ private List extractBids(BidderCall httpCall) { } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(FIRST_SEAT_BID_INDEX); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); return CollectionUtils.emptyIfNull(firstSeatBid.getBid()).stream() .filter(Objects::nonNull) .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) diff --git a/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java index af8c52bf8ab..a36057f2c33 100644 --- a/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java +++ b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java @@ -73,7 +73,7 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); return CollectionUtils.emptyIfNull(firstSeatBid.getBid()).stream() .filter(Objects::nonNull) .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) diff --git a/src/main/java/org/prebid/server/bidder/smrtconnect/SmrtconnectBidder.java b/src/main/java/org/prebid/server/bidder/smrtconnect/SmrtconnectBidder.java new file mode 100644 index 00000000000..1eb42956ec6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smrtconnect/SmrtconnectBidder.java @@ -0,0 +1,110 @@ +package org.prebid.server.bidder.smrtconnect; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.smrtconnect.ExtImpSmrtconnect; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SmrtconnectBidder implements Bidder { + + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + private static final TypeReference> SMRTCONNECT_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private final String endpointUrl; + private final JacksonMapper mapper; + + public SmrtconnectBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + final Imp firstImp = bidRequest.getImp().getFirst(); + final ExtImpSmrtconnect extImpSmrtconnect; + + try { + extImpSmrtconnect = mapper.mapper().convertValue(firstImp.getExt(), SMRTCONNECT_EXT_TYPE_REFERENCE) + .getBidder(); + } catch (IllegalArgumentException e) { + return Result.withError(BidderError.badInput("Ext.bidder not provided")); + } + + return Result.withValue( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveEndpoint(extImpSmrtconnect.getSupplyId())) + .headers(HttpUtil.headers()) + .body(mapper.encodeToBytes(bidRequest)) + .impIds(BidderUtil.impIds(bidRequest)) + .payload(bidRequest) + .build()); + } + + private String resolveEndpoint(String supplyId) { + return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(supplyId)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse("Bad Server Response")); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid.getMtype()), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Integer mType) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + + case null, default -> throw new PreBidException("Unsupported mType " + mType); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java b/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java index 42ad63cbd5e..2582e8a5eea 100644 --- a/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java @@ -3,16 +3,18 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -22,6 +24,7 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -34,10 +37,17 @@ public class SonobiBidder implements Bidder { new TypeReference<>() { }; + private static final String BIDDER_CURRENCY = "USD"; + + private final CurrencyConversionService currencyConversionService; private final String endpointUrl; private final JacksonMapper mapper; - public SonobiBidder(String endpointUrl, JacksonMapper mapper) { + public SonobiBidder(CurrencyConversionService currencyConversionService, + String endpointUrl, + JacksonMapper mapper) { + + this.currencyConversionService = currencyConversionService; this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -50,7 +60,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { final ExtImpSonobi extImpSonobi = parseImpExt(imp); - final Imp modifiedImp = modifyImp(imp, extImpSonobi.getTagId()); + final Imp modifiedImp = modifyImp(bidRequest, imp, extImpSonobi.getTagId()); requests.add(makeRequest(bidRequest, modifiedImp)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -68,34 +78,51 @@ private ExtImpSonobi parseImpExt(Imp imp) throws PreBidException { } } - private static Imp modifyImp(Imp imp, String tagId) { - return imp.toBuilder().tagid(tagId).build(); + private Imp modifyImp(BidRequest bidRequest, Imp imp, String tagId) { + final Price bidFloor = resolveBidFloor(bidRequest, imp); + return imp.toBuilder() + .tagid(tagId) + .bidfloor(bidFloor.getValue()) + .bidfloorcur(bidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + return Price.of( + BIDDER_CURRENCY, + currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY)); + } + + return Price.of(bidFloorCurrency, bidFloor); } private HttpRequest makeRequest(BidRequest bidRequest, Imp imp) { - final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(Collections.singletonList(imp)).build(); + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .cur(Collections.singletonList(BIDDER_CURRENCY)) + .imp(Collections.singletonList(imp)) + .build(); return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); } @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final BidResponse bidResponse; try { - bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - } catch (DecodeException e) { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } - - final List errors = new ArrayList<>(); - final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); - - return Result.of(bids, errors); } private static List extractBids(BidRequest bidRequest, - BidResponse bidResponse, - List errors) { + BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); @@ -107,26 +134,19 @@ private static List extractBids(BidRequest bidRequest, .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> makeBidderBid(bid, bidRequest.getImp(), bidResponse.getCur(), errors)) - .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), BIDDER_CURRENCY)) .toList(); } - private static BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { - try { - return BidderBid.of(bid, resolveBidType(bid.getImpid(), imps), currency); - } catch (PreBidException e) { - errors.add(BidderError.badServerResponse(e.getMessage())); - return null; - } - } - private static BidType resolveBidType(String impId, List imps) throws PreBidException { for (Imp imp : imps) { if (Objects.equals(impId, imp.getId())) { if (imp.getBanner() == null && imp.getVideo() != null) { return BidType.video; } + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() != null) { + return BidType.xNative; + } return BidType.banner; } } diff --git a/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java b/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java index c33ef88b520..cbd180f2e6f 100644 --- a/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java +++ b/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -151,13 +152,24 @@ private BidRequest createRequest(BidRequest request, List imps, ExtImpTaboo final List impExtBCat = impExt.getBCat(); final String impExtPageType = impExt.getPageType(); - final Site site = Optional.ofNullable(request.getSite()) - .map(Site::toBuilder) - .orElseGet(Site::builder) + final Publisher publisher = Publisher.builder().id(impExtPublisherId).build(); + + final Site site = request.getSite(); + final Site modifiedSite = site == null + ? null + : site.toBuilder() .id(impExtPublisherId) .name(impExtPublisherId) .domain(resolveDomain(impExt.getPublisherDomain(), request)) - .publisher(Publisher.builder().id(impExtPublisherId).build()) + .publisher(publisher) + .build(); + + final App app = request.getApp(); + final App modifiedApp = app == null + ? null + : app.toBuilder() + .id(impExtPublisherId) + .publisher(publisher) .build(); final ExtRequest extRequest = StringUtils.isNotEmpty(impExtPageType) @@ -166,7 +178,8 @@ private BidRequest createRequest(BidRequest request, List imps, ExtImpTaboo return request.toBuilder() .imp(imps) - .site(site) + .site(modifiedSite) + .app(modifiedApp) .badv(CollectionUtils.isNotEmpty(impExtBAdv) ? impExtBAdv : request.getBadv()) .bcat(CollectionUtils.isNotEmpty(impExtBCat) ? impExtBCat : request.getBcat()) .ext(extRequest) @@ -189,11 +202,11 @@ private ExtRequest createExtRequest(String pageType) { private HttpRequest createHttpRequest(MediaType type, BidRequest outgoingRequest) { return BidderUtil.defaultRequest(outgoingRequest, - buildEndpointUrl(outgoingRequest.getSite().getId(), type), + buildEndpointUrl(outgoingRequest, type), mapper); } - private String buildEndpointUrl(String publisherId, MediaType mediaType) { + private String buildEndpointUrl(BidRequest bidRequest, MediaType mediaType) { final String type = switch (mediaType) { case BANNER -> DISPLAY_ENDPOINT_PREFIX; case NATIVE -> NATIVE_ENDPOINT_PREFIX; @@ -202,6 +215,10 @@ private String buildEndpointUrl(String publisherId, MediaType mediaType) { default -> throw new AssertionError(); }; + final String publisherId = Optional.ofNullable(bidRequest.getSite()).map(Site::getId) + .or(() -> Optional.ofNullable(bidRequest.getApp()).map(App::getId)) + .orElse(StringUtils.EMPTY); + return endpointTemplate .replace("{{GvlID}}", gvlId) .replace("{{MediaType}}", type) diff --git a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java index 846101e0685..ac511bb03b9 100644 --- a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java +++ b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java @@ -62,7 +62,7 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpTappx extImpTappx; final String url; try { - extImpTappx = parseImpExt(imps.get(0)); + extImpTappx = parseImpExt(imps.getFirst()); url = resolveUrl(extImpTappx, request.getTest()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -82,7 +82,7 @@ private ExtImpTappx parseImpExt(Imp imp) { private static List modifyImps(List imps, ExtImpTappx extImpTappx) { final List modifiedImps = new ArrayList<>(imps); - modifiedImps.set(0, modifyImp(imps.get(0), extImpTappx)); + modifiedImps.set(0, modifyImp(imps.getFirst(), extImpTappx)); return modifiedImps; } diff --git a/src/main/java/org/prebid/server/bidder/teads/TeadsBidder.java b/src/main/java/org/prebid/server/bidder/teads/TeadsBidder.java index dd745e668a2..05a9b72a51e 100644 --- a/src/main/java/org/prebid/server/bidder/teads/TeadsBidder.java +++ b/src/main/java/org/prebid/server/bidder/teads/TeadsBidder.java @@ -82,7 +82,7 @@ private static Banner modifyBanner(Banner banner) { if (banner != null) { final List format = banner.getFormat(); if (CollectionUtils.isNotEmpty(format)) { - final Format firstFormat = format.get(0); + final Format firstFormat = format.getFirst(); return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); } } diff --git a/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java b/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java index 516dbacfcf9..b65f4f45982 100644 --- a/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java +++ b/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java @@ -61,7 +61,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final String publisherId = getPublisherId(bidRequest); final String seatCode; final ExtImpTelaria extImp; - Imp modifyImp = bidRequest.getImp().get(0); + Imp modifyImp = bidRequest.getImp().getFirst(); try { extImp = parseImpExt(modifyImp); @@ -193,7 +193,7 @@ private List extractBids(BidResponse bidResponse, BidRequest bidReque } private static List bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) { - final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final SeatBid firstSeatBid = bidResponse.getSeatbid().getFirst(); final List bids = firstSeatBid.getBid(); final List imps = bidRequest.getImp(); diff --git a/src/main/java/org/prebid/server/bidder/theadx/TheadxBidder.java b/src/main/java/org/prebid/server/bidder/theadx/TheadxBidder.java new file mode 100644 index 00000000000..d144eeca773 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/theadx/TheadxBidder.java @@ -0,0 +1,159 @@ +package org.prebid.server.bidder.theadx; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.theadx.ExtImpTheadx; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class TheadxBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String OPENRTB_VERSION = "2.5"; + private static final String X_TEST_HEADER = "X-TEST"; + private static final String X_TEST = "1"; + private static final String X_DEVICE_USER_AGENT_HEADER = "X-Device-User-Agent"; + private static final String X_REAL_IP_HEADER = "X-Real-IP"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public TheadxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpTheadx extImp = parseImpExt(imp); + modifiedImps.add(imp.toBuilder().tagid(extImp.getTagId()).build()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); + return Result.of(Collections.singletonList(makeHttpRequest(modifiedRequest)), errors); + } + + private ExtImpTheadx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private HttpRequest makeHttpRequest(BidRequest request) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(this.endpointUrl) + .impIds(BidderUtil.impIds(request)) + .headers(makeHeaders(request)) + .payload(request) + .body(mapper.encodeToBytes(request)) + .build(); + } + + private static MultiMap makeHeaders(BidRequest bidRequest) { + final Device device = bidRequest.getDevice(); + final MultiMap headers = HttpUtil.headers(); + + headers.set(HttpUtil.X_OPENRTB_VERSION_HEADER, OPENRTB_VERSION); + headers.set(X_TEST_HEADER, X_TEST); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, X_DEVICE_USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, X_REAL_IP_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, X_REAL_IP_HEADER, device.getIp()); + } + + return headers; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || bidResponse.getSeatbid() == null) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType mediaType = getMediaType(bid, errors); + return mediaType == null ? null : BidderBid.of(bid, mediaType, currency); + } + + private BidType getMediaType(Bid bid, List errors) { + try { + return Optional.ofNullable(bid.getExt()) + .map(ext -> mapper.mapper().convertValue(ext, EXT_PREBID_TYPE_REFERENCE)) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(IllegalArgumentException::new); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badServerResponse( + "Failed to parse impression \"%s\" mediatype".formatted(bid.getImpid()))); + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java new file mode 100644 index 00000000000..c9a8366f9ac --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -0,0 +1,204 @@ +package org.prebid.server.bidder.thetradedesk; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +public class TheTradeDeskBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PREBID_INTEGRATION_TYPE_HEADER = "x-integration-type"; + private static final String PREBID_INTEGRATION_TYPE = "1"; + private static final MultiMap HEADERS = HttpUtil.headers() + .add(PREBID_INTEGRATION_TYPE_HEADER, PREBID_INTEGRATION_TYPE); + + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$"); + + private final String endpointUrl; + private final String supplyId; + private final JacksonMapper mapper; + + public TheTradeDeskBidder(String endpointUrl, JacksonMapper mapper, String supplyId) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.supplyId = validateSupplyId(supplyId); + this.mapper = Objects.requireNonNull(mapper); + } + + private static String validateSupplyId(String supplyId) { + if (StringUtils.isBlank(supplyId) || SUPPLY_ID_PATTERN.matcher(supplyId).matches()) { + return supplyId; + } + + throw new IllegalArgumentException("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + + String publisherId = null; + for (Imp imp : request.getImp()) { + try { + final ExtImpTheTradeDesk extImp = parseImpExt(imp); + publisherId = publisherId == null + ? StringUtils.isNotBlank(extImp.getPublisherId()) + ? extImp.getPublisherId() + : publisherId + : publisherId; + + modifiedImps.add(modifyImp(imp)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + HEADERS, + resolveEndpoint(), + mapper); + + return Result.withValue(httpRequest); + } + + private ExtImpTheTradeDesk parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); + } + } + + private static Imp modifyImp(Imp imp) { + final Banner banner = imp.getBanner(); + + if (banner != null && CollectionUtils.isNotEmpty(banner.getFormat())) { + final Format format = banner.getFormat().getFirst(); + return imp.toBuilder() + .banner(banner.toBuilder().w(format.getW()).h(format.getH()).build()) + .build(); + } + + return imp; + } + + private static BidRequest modifyRequest(BidRequest request, List modifiedImps, String publisherId) { + return request.toBuilder() + .imp(modifiedImps) + .site(modifySite(request, publisherId)) + .app(modifyApp(request, publisherId)) + .build(); + } + + private static Site modifySite(BidRequest request, String publisherId) { + final Site site = request.getSite(); + if (site == null) { + return null; + } + + return site.toBuilder() + .publisher(modifyPublisher(site.getPublisher(), publisherId)) + .build(); + } + + private static Publisher modifyPublisher(Publisher publisher, String publisherId) { + if (publisher == null) { + return Publisher.builder().id(publisherId).build(); + } + + return publisher.toBuilder() + .id(StringUtils.isNotBlank(publisherId) ? publisherId : publisher.getId()) + .build(); + } + + private static App modifyApp(BidRequest request, String publisherId) { + final Site site = request.getSite(); + final App app = request.getApp(); + + if (site != null) { + return app; + } + + if (app == null) { + return null; + } + + return app.toBuilder() + .publisher(modifyPublisher(app.getPublisher(), publisherId)) + .build(); + } + + private String resolveEndpoint() { + return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(supplyId))); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java b/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java new file mode 100644 index 00000000000..99d8bc8e74c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java @@ -0,0 +1,135 @@ +package org.prebid.server.bidder.tradplus; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.tradplus.ExtImpTradPlus; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class TradPlusBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + public static final String X_OPENRTB_VERSION = "2.5"; + + private static final String ZONE_ID = "{{ZoneID}}"; + private static final String ACCOUNT_ID = "{{AccountID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public TradPlusBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpTradPlus extImpTradPlus = parseImpExt(bidRequest.getImp().getFirst().getExt()); + validateImpExt(extImpTradPlus); + final HttpRequest httpRequest; + httpRequest = makeHttpRequest(extImpTradPlus, bidRequest.getImp(), bidRequest); + return Result.withValue(httpRequest); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpTradPlus parseImpExt(ObjectNode extNode) { + final ExtImpTradPlus extImpTradPlus; + try { + extImpTradPlus = mapper.mapper().convertValue(extNode, EXT_TYPE_REFERENCE).getBidder(); + return extImpTradPlus; + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private void validateImpExt(ExtImpTradPlus extImpTradPlus) { + if (StringUtils.isBlank(extImpTradPlus.getAccountId())) { + throw new PreBidException("Invalid/Missing AccountID"); + } + } + + private HttpRequest makeHttpRequest(ExtImpTradPlus extImpTradPlus, List imps, + BidRequest bidRequest) { + final String uri; + uri = endpointUrl.replace(ZONE_ID, extImpTradPlus.getZoneId()).replace(ACCOUNT_ID, + extImpTradPlus.getAccountId()); + + final BidRequest outgoingRequest = bidRequest.toBuilder().imp(removeImpsExt(imps)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, makeHeaders(), uri, mapper); + } + + private MultiMap makeHeaders() { + return HttpUtil.headers().set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + } + + private static List removeImpsExt(List imps) { + return imps.stream().map(imp -> imp.toBuilder().ext(null).build()).toList(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse, httpCall.getRequest().getPayload())); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, BidRequest bidRequest) { + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections + .emptyList() : bidsFromResponse(bidResponse, bidRequest.getImp()); + } + + private static List bidsFromResponse(BidResponse bidResponse, List imps) { + return bidResponse.getSeatbid().stream().filter(Objects::nonNull).map(SeatBid::getBid) + .filter(Objects::nonNull).flatMap(Collection::stream).map(bid -> BidderBid + .of(bid, getBidType(bid.getImpid(), imps), bidResponse.getCur())).toList(); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return BidType.banner; + } + } + throw new PreBidException( + "Invalid bid imp ID #%s does not match any imp IDs from the original bid request".formatted(impId)); + } + +} diff --git a/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java index 653e2beef38..b7ff310a7cf 100644 --- a/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java +++ b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.tripleliftnative; -import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -19,7 +18,6 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; import org.prebid.server.proto.openrtb.ext.request.triplelift.ExtImpTriplelift; @@ -27,19 +25,19 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; public class TripleliftNativeBidder implements Bidder { private static final String UNKNOWN_PUBLISHER_ID = "unknown"; - private static final TypeReference> TRIPLELIFT_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; + private static final String MSN_DOMAIN = "msn.com"; private final String endpointUrl; private final List publisherWhiteList; @@ -55,9 +53,13 @@ public TripleliftNativeBidder(String endpointUrl, List publisherWhiteLis public Result>> makeHttpRequests(BidRequest bidRequest) { final List errors = new ArrayList<>(); final List validImps = new ArrayList<>(); + final boolean hasMsnDomain = hasMsnDomain(bidRequest); + for (Imp imp : bidRequest.getImp()) { try { - validImps.add(modifyImp(imp)); + validateImp(imp); + final TripleliftNativeExtImp impExt = parseExtImp(imp); + validImps.add(modifyImp(imp, impExt, hasMsnDomain)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -82,26 +84,47 @@ public Result>> makeHttpRequests(BidRequest bidRequ errors); } - private Imp modifyImp(Imp imp) throws PreBidException { + private static boolean hasMsnDomain(BidRequest bidRequest) { + final boolean hasMsnDomainInSite = Optional.ofNullable(bidRequest.getSite()) + .map(Site::getPublisher) + .map(Publisher::getDomain) + .map(MSN_DOMAIN::equals) + .orElse(false); + + final boolean hasMsnDomainInApp = Optional.ofNullable(bidRequest.getApp()) + .map(App::getPublisher) + .map(Publisher::getDomain) + .map(MSN_DOMAIN::equals) + .orElse(false); + + return hasMsnDomainInSite || hasMsnDomainInApp; + } + + private static void validateImp(Imp imp) { if (imp.getXNative() == null) { throw new PreBidException("no native object specified"); } + } - final ExtImpTriplelift impExt = parseExtImpTriplelift(imp); - final String inventoryCode = impExt.getInventoryCode(); - if (StringUtils.isBlank(inventoryCode)) { - throw new PreBidException("no inv_code specified"); - } + private Imp modifyImp(Imp imp, TripleliftNativeExtImp impExt, boolean hasMsnDomain) throws PreBidException { + final ExtImpTriplelift impExtBidder = impExt.getBidder(); + final TripleliftNativeExtImpData data = impExt.getData(); + + final BigDecimal bidFloor = impExtBidder.getFloor(); + final boolean hasTagCodeInData = Optional.ofNullable(data) + .map(TripleliftNativeExtImpData::getTagCode) + .map(StringUtils::isNotBlank) + .orElse(false); return imp.toBuilder() - .tagid(inventoryCode) - .bidfloor(impExt.getFloor()) + .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) + .tagid(hasTagCodeInData && hasMsnDomain ? data.getTagCode() : impExtBidder.getInventoryCode()) .build(); } - private ExtImpTriplelift parseExtImpTriplelift(Imp imp) { + private TripleliftNativeExtImp parseExtImp(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), TRIPLELIFT_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), TripleliftNativeExtImp.class); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage(), e); } diff --git a/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeExtImp.java b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeExtImp.java new file mode 100644 index 00000000000..e3c9581f3f8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeExtImp.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.tripleliftnative; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.triplelift.ExtImpTriplelift; + +@Value(staticConstructor = "of") +public class TripleliftNativeExtImp { + + ExtImpTriplelift bidder; + + TripleliftNativeExtImpData data; +} diff --git a/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeExtImpData.java b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeExtImpData.java new file mode 100644 index 00000000000..bf97c198291 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeExtImpData.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.tripleliftnative; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class TripleliftNativeExtImpData { + + @JsonProperty("tag_code") + String tagCode; +} diff --git a/src/main/java/org/prebid/server/bidder/trustedstack/TrustedstackBidder.java b/src/main/java/org/prebid/server/bidder/trustedstack/TrustedstackBidder.java new file mode 100644 index 00000000000..1a10bd933bb --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/trustedstack/TrustedstackBidder.java @@ -0,0 +1,127 @@ +package org.prebid.server.bidder.trustedstack; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class TrustedstackBidder implements Bidder { + + private static final String EXTERNAL_URL_MACRO = "{{PREBID_SERVER_ENDPOINT}}"; + private final String endpointUrl; + private final JacksonMapper mapper; + + public TrustedstackBidder(String endpointUrl, String externalUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(resolveEndpoint(endpointUrl, externalUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); + + return Result.of(bids, errors); + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse, + List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final String currency = bidResponse.getCur(); + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidRequest.getImp(), currency, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidType resolveBidType(Bid bid, List imps) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + return resolveBidTypeFromImpId(bid.getImpid(), imps); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> + throw new PreBidException("Unable to fetch mediaType: %s" + .formatted(bid.getImpid())); + }; + } + + private static BidderBid makeBidderBid(Bid bid, List imps, String cur, List errors) { + final BidType bidType; + try { + bidType = resolveBidType(bid, imps); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, cur); + } + + private static BidType resolveBidTypeFromImpId(String impId, List imps) { + for (Imp imp : imps) { + if (Objects.equals(impId, imp.getId())) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getAudio() != null) { + return BidType.audio; + } + } + } + + return BidType.banner; + } + + private String resolveEndpoint(String endpointUrl, String externalUrl) { + return Objects.requireNonNull(endpointUrl).replace(EXTERNAL_URL_MACRO, HttpUtil.encodeUrl(externalUrl)); + } +} diff --git a/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java b/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java index 3e6548f3e21..ae3e814c249 100644 --- a/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java +++ b/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java @@ -52,7 +52,7 @@ public Result>> makeHttpRequests(BidRequest request String partnerId = null; try { - final ExtImpUcfunnel extImpUcfunnel = parseImpExt(request.getImp().get(0)); + final ExtImpUcfunnel extImpUcfunnel = parseImpExt(request.getImp().getFirst()); final String adUnitId = extImpUcfunnel.getAdunitid(); partnerId = extImpUcfunnel.getPartnerid(); if (StringUtils.isEmpty(partnerId) || StringUtils.isEmpty(adUnitId)) { diff --git a/src/main/java/org/prebid/server/bidder/undertone/UndertoneBidder.java b/src/main/java/org/prebid/server/bidder/undertone/UndertoneBidder.java index 99e2d20e733..2ba1663a87e 100644 --- a/src/main/java/org/prebid/server/bidder/undertone/UndertoneBidder.java +++ b/src/main/java/org/prebid/server/bidder/undertone/UndertoneBidder.java @@ -204,7 +204,7 @@ private Map getIdImpMap(BidRequest bidRequest) { .collect(Collectors.groupingBy(Imp::getId)) .entrySet() .stream() - .collect(Collectors.toMap(Map.Entry::getKey, imps -> imps.getValue().get(0))); + .collect(Collectors.toMap(Map.Entry::getKey, imps -> imps.getValue().getFirst())); } private BidType getBidType(Bid bid, Map idImpMap) { diff --git a/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java b/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java index 8c1acc49751..110786c683a 100644 --- a/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java +++ b/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java @@ -61,7 +61,7 @@ public Result>> makeHttpRequests(BidRequest request try { validateRegs(request.getRegs()); - firstImpExt = parseImpExt(request.getImp().get(0)).getBidder(); + firstImpExt = parseImpExt(request.getImp().getFirst()).getBidder(); modifiedImps = request.getImp().stream().map(this::modifyImp).toList(); modifiedSource = modifySource(request.getSource()); modifiedApp = modifyApp(request.getApp(), firstImpExt.getMediaId(), firstImpExt.getPublisherId()); diff --git a/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java b/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java index f6e97a95dee..ee0471a937f 100644 --- a/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java +++ b/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java @@ -1,6 +1,8 @@ package org.prebid.server.bidder.unruly; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; @@ -19,6 +21,8 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.unruly.ExtImpUnruly; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -84,18 +88,18 @@ public Result> makeBids(BidderCall httpCall, BidRequ } } - private static List extractBids(BidRequest bidRequest, - BidResponse bidResponse, - List errors) { + private List extractBids(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidRequest, bidResponse, errors); } - private static List bidsFromResponse(BidRequest bidRequest, - BidResponse bidResponse, - List errors) { + private List bidsFromResponse(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) @@ -107,9 +111,13 @@ private static List bidsFromResponse(BidRequest bidRequest, .toList(); } - private static BidderBid resolveBidderBid(Bid bid, String currency, List imps, List errors) { + private BidderBid resolveBidderBid(Bid bid, String currency, List imps, List errors) { try { - return BidderBid.of(bid, getBidType(bid.getImpid(), imps), currency); + final BidType bidType = getBidType(bid.getImpid(), imps); + return BidderBid.of( + bidType == BidType.video ? resolveBid(bid) : bid, + getBidType(bid.getImpid(), imps), + currency); } catch (PreBidException e) { errors.add(BidderError.badServerResponse(e.getMessage())); return BidderBid.of(bid, BidType.banner, currency); @@ -135,4 +143,32 @@ private static BidType getBidType(String impId, List imps) { throw new PreBidException( "Bid response imp ID " + impId + " not found in bid request containing imps" + unmatchedImpIds); } + + private Bid resolveBid(Bid bid) { + final Integer duration = bid.getDur(); + if (duration == null || duration <= 0) { + return bid; + } + + return bid.toBuilder().ext(resolveBidExt(duration, bid.getExt())).build(); + } + + private ObjectNode resolveBidExt(Integer duration, ObjectNode bidExt) { + final ObjectNode bidExtUpdated = bidExt != null && !bidExt.isMissingNode() + ? bidExt + : mapper.mapper().createObjectNode(); + final JsonNode bidExtPrebid = resolveBidExtPrebid(duration, bidExtUpdated.get("prebid")); + + return bidExtUpdated.set("prebid", bidExtPrebid); + } + + private ObjectNode resolveBidExtPrebid(Integer duration, JsonNode bidExtPrebid) { + final ExtBidPrebidVideo extBidPrebidVideo = ExtBidPrebidVideo.of(duration, null); + if (bidExtPrebid == null || bidExtPrebid.isMissingNode()) { + return mapper.mapper().valueToTree(ExtBidPrebid.builder().video(extBidPrebidVideo).build()); + } + + final ObjectNode bidExtPrebidCasted = (ObjectNode) bidExtPrebid; + return bidExtPrebidCasted.set("video", mapper.mapper().valueToTree(extBidPrebidVideo)); + } } diff --git a/src/main/java/org/prebid/server/bidder/vidazoo/VidazooBidder.java b/src/main/java/org/prebid/server/bidder/vidazoo/VidazooBidder.java new file mode 100644 index 00000000000..eda63f53d2f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/vidazoo/VidazooBidder.java @@ -0,0 +1,127 @@ +package org.prebid.server.bidder.vidazoo; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.vidazoo.VidazooImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class VidazooBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public VidazooBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final VidazooImpExt impExt = parseImpExt(imp); + requests.add(makeHttpRequest(bidRequest, imp, impExt)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private VidazooImpExt parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private HttpRequest makeHttpRequest(BidRequest bidRequest, Imp imp, VidazooImpExt impExt) { + final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(Collections.singletonList(imp)).build(); + final String uri = endpointUrl + HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getConnectionId()).trim()); + + return BidderUtil.defaultRequest(modifiedBidRequest, uri, mapper); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + final BidType mediaType = getBidMediaType(bid); + return BidderBid.of(bid, mediaType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + default -> throw new PreBidException("Could not define bid type for imp: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/videoheroes/VideoHeroesBidder.java b/src/main/java/org/prebid/server/bidder/videoheroes/VideoHeroesBidder.java index 015e0b4b755..7ce127d79cd 100644 --- a/src/main/java/org/prebid/server/bidder/videoheroes/VideoHeroesBidder.java +++ b/src/main/java/org/prebid/server/bidder/videoheroes/VideoHeroesBidder.java @@ -47,7 +47,7 @@ public VideoHeroesBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List requestImps = request.getImp(); - final Imp firstImp = requestImps.get(FIRST_IMP_INDEX); + final Imp firstImp = requestImps.getFirst(); final ExtImpVideoHeroes impExt; try { @@ -61,7 +61,7 @@ public Result>> makeHttpRequests(BidRequest request private static List modifyFirstImp(List imp) { final List modifiedImps = new ArrayList<>(imp); - final Imp modifiedFirstImp = imp.get(FIRST_IMP_INDEX).toBuilder().ext(null).build(); + final Imp modifiedFirstImp = imp.getFirst().toBuilder().ext(null).build(); modifiedImps.set(FIRST_IMP_INDEX, modifiedFirstImp); return modifiedImps; diff --git a/src/main/java/org/prebid/server/bidder/vidoomy/VidoomyBidder.java b/src/main/java/org/prebid/server/bidder/vidoomy/VidoomyBidder.java index 0784be520c1..68cb8709694 100644 --- a/src/main/java/org/prebid/server/bidder/vidoomy/VidoomyBidder.java +++ b/src/main/java/org/prebid/server/bidder/vidoomy/VidoomyBidder.java @@ -72,7 +72,7 @@ private static Imp modifyImp(Imp imp) { validateBannerSizes(width, height, formats); final boolean useFormatSize = width == null || height == null; - final Format firstFormat = useFormatSize ? formats.get(0) : null; + final Format firstFormat = useFormatSize ? formats.getFirst() : null; return imp.toBuilder() .banner(banner.toBuilder() .w(useFormatSize ? zeroIfFormatMeasureNull(firstFormat, Format::getW) : width) diff --git a/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java new file mode 100644 index 00000000000..f2c87bc2583 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java @@ -0,0 +1,174 @@ +package org.prebid.server.bidder.vungle; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.vungle.model.VungleImpressionExt; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ObjectUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class VungleBidder implements Bidder { + + private static final String BIDDER_CURRENCY = "USD"; + private static final String X_OPENRTB_VERSION = "2.5"; + + private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + + public VungleBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final Price price = resolveBidFloor(imp, bidRequest); + final VungleImpressionExt impExt = parseImpExt(imp); + final VungleImpressionExt modifiedImpExt = modifyImpExt(impExt, bidRequest); + final Imp modifiedImp = modifyImp(imp, modifiedImpExt, price); + final BidRequest modifiedRequest = modifyBidRequest( + bidRequest, + modifiedImp, + modifiedImpExt.getBidder().getAppStoreId()); + + httpRequests.add(makeHttpRequest(modifiedRequest)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + BigDecimal bigDecimal = null; + if (BidderUtil.isValidPrice(imp.getBidfloor()) + && !StringUtils.equalsIgnoreCase(imp.getBidfloorcur(), BIDDER_CURRENCY) + && StringUtils.isNotBlank(imp.getBidfloorcur())) { + bigDecimal = currencyConversionService.convertCurrency( + imp.getBidfloor(), bidRequest, imp.getBidfloorcur(), BIDDER_CURRENCY); + } + + return Price.of(BIDDER_CURRENCY, bigDecimal); + } + + private VungleImpressionExt parseImpExt(Imp imp) { + return mapper.mapper().convertValue(imp.getExt(), VungleImpressionExt.class); + } + + private static VungleImpressionExt modifyImpExt(VungleImpressionExt impExt, BidRequest bidRequest) { + final ExtImpVungle bidder = impExt.getBidder(); + final String buyerId = ObjectUtil.getIfNotNull(bidRequest.getUser(), User::getBuyeruid); + final ExtImpVungle vungle = ExtImpVungle.of( + buyerId, + bidder.getAppStoreId(), + bidder.getPlacementReferenceId()); + + return impExt.toBuilder().vungle(vungle).build(); + } + + private Imp modifyImp(Imp imp, VungleImpressionExt modifiedImpExt, Price price) { + return imp.toBuilder() + .tagid(modifiedImpExt.getBidder().getPlacementReferenceId()) + .ext(mapper.mapper().convertValue(modifiedImpExt, ObjectNode.class)) + .bidfloor(price.getValue() != null ? price.getValue() : imp.getBidfloor()) + .bidfloorcur(price.getValue() != null ? price.getCurrency() : imp.getBidfloorcur()) + .build(); + } + + private static BidRequest modifyBidRequest(BidRequest bidRequest, Imp imp, String appStoreId) { + final App app = bidRequest.getApp(); + final Site site = bidRequest.getSite(); + if (app == null && site == null) { + throw new PreBidException("The bid request must have an app or site object"); + } + return bidRequest.toBuilder() + .imp(Collections.singletonList(imp)) + .app(app == null ? App.builder().id(appStoreId).build() : app.toBuilder().id(appStoreId).build()) + .site(null) + .build(); + } + + private HttpRequest makeHttpRequest(BidRequest request) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .impIds(BidderUtil.impIds(request)) + .headers(headers()) + .payload(request) + .body(mapper.encodeToBytes(request)) + .build(); + } + + private static MultiMap headers() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, BidType.video, bidResponse.getCur())) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java b/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java new file mode 100644 index 00000000000..267d7368768 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.vungle.model; + +import lombok.Builder; +import lombok.Getter; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; + +@Builder(toBuilder = true) +@Getter +public class VungleImpressionExt { + + ExtImpPrebid prebid; + + ExtImpVungle bidder; + + ExtImpVungle vungle; +} diff --git a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java index 9d35a90c69d..3e15eb554a3 100644 --- a/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/yahooads/YahooAdsBidder.java @@ -162,7 +162,7 @@ private static Banner modifyBanner(Banner banner) { if (CollectionUtils.isEmpty(bannerFormats)) { throw new PreBidException("No sizes provided for Banner"); } - final Format firstFormat = bannerFormats.get(0); + final Format firstFormat = bannerFormats.getFirst(); return banner.toBuilder() .w(firstFormat.getW()) @@ -251,10 +251,6 @@ private static List extractBids(BidResponse bidResponse, BidRequest b if (seatBids == null) { return Collections.emptyList(); } - - if (seatBids.isEmpty()) { - throw new PreBidException("Invalid SeatBids count: 0"); - } return bidsFromResponse(bidResponse, bidRequest.getImp()); } diff --git a/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java b/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java index b5794ce7368..46ed20a06f1 100644 --- a/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java +++ b/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java @@ -17,8 +17,8 @@ import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; @@ -86,7 +86,7 @@ private static String getReferer(BidRequest request) { private static String getCurrency(BidRequest request) { final List currencies = request.getCur(); - final String currency = CollectionUtils.isNotEmpty(currencies) ? currencies.get(0) : null; + final String currency = CollectionUtils.isNotEmpty(currencies) ? currencies.getFirst() : null; return StringUtils.defaultString(currency); } @@ -126,7 +126,7 @@ private static Banner modifyBanner(Banner banner) { final List format = banner.getFormat(); if (weight == null || height == null || weight == 0 || height == 0) { if (CollectionUtils.isNotEmpty(format)) { - final Format firstFormat = format.get(0); + final Format firstFormat = format.getFirst(); return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); } throw new PreBidException("Invalid sizes provided for Banner %sx%s".formatted(weight, height)); diff --git a/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java b/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java index 2705b1f7c6e..c33de175781 100644 --- a/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java @@ -9,7 +9,6 @@ import com.iab.openrtb.request.Native; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.vertx.core.http.HttpMethod; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -21,6 +20,9 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.yeahmobi.ExtImpYeahmobi; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -40,6 +42,7 @@ public class YeahmobiBidder implements Bidder { }; private static final String HOST_MACRO = "{{Host}}"; private static final String HOST_PATTERN = "gw-%s-bid.yeahtargeter.com"; + private static final String NATIVE = "native"; private final String endpointUrl; private final JacksonMapper mapper; @@ -65,7 +68,7 @@ public Result>> makeHttpRequests(BidRequest request } if (extImp == null) { - return Result.withError(BidderError.badInput("Invalid ExtImpYeahmobi value")); + return Result.withErrors(errors); } final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); @@ -74,50 +77,53 @@ public Result>> makeHttpRequests(BidRequest request return Result.of(Collections.singletonList(httpRequest), errors); } - private HttpRequest makeHttpRequest(BidRequest request, String zoneId) { - final String host = HOST_PATTERN.formatted(zoneId); - final String uri = endpointUrl.replace(HOST_MACRO, host); - - return HttpRequest.builder() - .method(HttpMethod.POST) - .uri(uri) - .impIds(BidderUtil.impIds(request)) - .headers(HttpUtil.headers()) - .payload(request) - .body(mapper.encodeToBytes(request)) - .build(); - } - private ExtImpYeahmobi parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(String.format("Impression id=%s, has invalid Ext", imp.getId())); + throw new PreBidException(e.getMessage()); } } private Imp modifyImp(Imp imp) { final Native impNative = imp.getXNative(); - return Optional.ofNullable(impNative) - .map(xNative -> resolveNativeRequest(xNative.getRequest())) - .map(nativeRequest -> imp.toBuilder().xNative( - impNative.toBuilder().request(nativeRequest).build()) - .build()) - .orElse(imp); + final String resolvedNativeRequest = impNative != null + ? resolveNativeRequest(impNative.getRequest()) + : null; + + return resolvedNativeRequest != null + ? imp.toBuilder() + .xNative(impNative.toBuilder().request(resolvedNativeRequest).build()) + .build() + : imp; } private String resolveNativeRequest(String nativeRequest) { + if (nativeRequest == null) { + return null; + } + try { - final JsonNode nativePayload = nativeRequest != null - ? mapper.mapper().readValue(nativeRequest, JsonNode.class) - : mapper.mapper().createObjectNode(); - final ObjectNode objectNode = mapper.mapper().createObjectNode().set("native", nativePayload); + final JsonNode nativePayload = mapper.mapper().readValue(nativeRequest, JsonNode.class); + + if (nativeRequest.contains(NATIVE)) { + return nativeRequest; + } + + final ObjectNode objectNode = mapper.mapper().createObjectNode().set(NATIVE, nativePayload); return mapper.mapper().writeValueAsString(objectNode); } catch (JsonProcessingException e) { - throw new PreBidException(e.getMessage()); + return null; } } + private HttpRequest makeHttpRequest(BidRequest request, String zoneId) { + return BidderUtil.defaultRequest( + request, + endpointUrl.replace(HOST_MACRO, HOST_PATTERN.formatted(zoneId)), + mapper); + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { @@ -132,19 +138,55 @@ private List extractBids(BidRequest bidRequest, BidResponse bidRespon if (bidResponse == null || bidResponse.getSeatbid() == null) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); - } - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { final Map impMap = bidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, Function.identity())); + return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidderUtil.getBidType(bid, impMap), bidResponse.getCur())) + .filter(Objects::nonNull) + .map(bid -> BidderBid.builder() + .bid(bid) + .type(getBidType(bid.getImpid(), impMap)) + .bidCurrency(bidResponse.getCur()) + .videoInfo(videoInfo(parseBidExt(bid.getExt()))) + .build()) .toList(); } + private static BidType getBidType(String impId, Map impIdToImp) { + if (impId == null) { + return BidType.banner; + } + + final Imp imp = impIdToImp.get(impId); + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else { + return BidType.banner; + } + } + + private ExtBidPrebid parseBidExt(ObjectNode bidExt) { + try { + return mapper.mapper().treeToValue(bidExt, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + throw new PreBidException("bid.ext json unmarshal error"); + } + } + + private ExtBidPrebidVideo videoInfo(ExtBidPrebid extBidPrebid) { + return Optional.ofNullable(extBidPrebid) + .map(ExtBidPrebid::getVideo) + .map(ExtBidPrebidVideo::getDuration) + .map(duration -> ExtBidPrebidVideo.of(duration, null)) + .orElse(null); + } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index bd957cd0ee0..cf99e41de84 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -14,6 +16,7 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -23,11 +26,17 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.yieldlab.model.YieldlabDigitalServicesActResponse; import org.prebid.server.bidder.yieldlab.model.YieldlabResponse; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -40,14 +49,19 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; public class YieldlabBidder implements Bidder { + private static final Logger logger = LoggerFactory.getLogger(YieldlabBidder.class); private static final TypeReference> YIELDLAB_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -58,6 +72,9 @@ public class YieldlabBidder implements Bidder { private static final String CREATIVE_ID = "%s%s%s"; private static final String AD_SOURCE_BANNER = ""; private static final String AD_SOURCE_URL = "https://ad.yieldlab.net/d/%s/%s/%s?%s"; + private static final String TRANSPARENCY_TEMPLATE = "%s~%s"; + private static final String TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER = "_"; + private static final String TRANSPARENCY_TEMPLATE_DELIMITER = "~~"; private static final String VAST_MARKUP = """ Yieldlab @@ -151,6 +168,12 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { .addParameter("ts", timestamp) .addParameter("t", getTargetingValues(extImpYieldlab)); + final String formats = makeFormats(request, extImpYieldlab); + + if (formats != null) { + uriBuilder.addParameter("sizes", formats); + } + final User user = request.getUser(); if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { uriBuilder.addParameter("ids", String.join("ylid:", user.getBuyeruid())); @@ -189,9 +212,29 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { uriBuilder.addParameter("consent", consent); } + extractDsaRequestParamsFromBidRequest(request).forEach(uriBuilder::addParameter); + return uriBuilder.toString(); } + private String makeFormats(BidRequest request, ExtImpYieldlab extImp) { + final List formats = new ArrayList<>(); + for (Imp imp: request.getImp()) { + if (isBanner(imp)) { + Stream.ofNullable(imp.getBanner().getFormat()) + .flatMap(Collection::stream) + .map(format -> "%s:%d|%d".formatted(extImp.getAdslotId(), format.getW(), format.getH())) + .forEach(formats::add); + } + } + + return formats.isEmpty() ? null : String.join(",", formats); + } + + private boolean isBanner(Imp imp) { + return imp.getBanner() != null && imp.getXNative() == null && imp.getVideo() == null && imp.getAudio() == null; + } + /** * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. */ @@ -231,6 +274,63 @@ private static String getConsentParameter(User user) { return ObjectUtils.defaultIfNull(consent, ""); } + private static Map extractDsaRequestParamsFromBidRequest(BidRequest request) { + return Optional.ofNullable(request.getRegs()) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .map(YieldlabBidder::extractDsaRequestParamsFromDsaRegsExtension) + .orElse(Collections.emptyMap()); + } + + private static Map extractDsaRequestParamsFromDsaRegsExtension(final ExtRegsDsa dsa) { + final Map dsaRequestParams = new HashMap<>(); + + if (dsa.getDsaRequired() != null) { + dsaRequestParams.put("dsarequired", dsa.getDsaRequired().toString()); + } + + if (dsa.getPubRender() != null) { + dsaRequestParams.put("dsapubrender", dsa.getPubRender().toString()); + } + + if (dsa.getDataToPub() != null) { + dsaRequestParams.put("dsadatatopub", dsa.getDataToPub().toString()); + } + + final List dsaTransparency = dsa.getTransparency(); + if (CollectionUtils.isNotEmpty(dsaTransparency)) { + final String encodedTransparencies = encodeTransparenciesAsString(dsaTransparency); + if (StringUtils.isNotBlank(encodedTransparencies)) { + dsaRequestParams.put("dsatransparency", encodedTransparencies); + } + } + + return dsaRequestParams; + } + + private static String encodeTransparenciesAsString(List transparencies) { + return transparencies.stream() + .filter(YieldlabBidder::isTransparencyValid) + .map(YieldlabBidder::encodeTransparency) + .collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER)); + } + + private static boolean isTransparencyValid(DsaTransparency transparency) { + return StringUtils.isNotBlank(transparency.getDomain()) + && transparency.getDsaParams() != null + && CollectionUtils.isNotEmpty(transparency.getDsaParams()); + } + + private static String encodeTransparency(DsaTransparency transparency) { + return TRANSPARENCY_TEMPLATE.formatted(transparency.getDomain(), + encodeTransparencyParams(transparency.getDsaParams())); + } + + private static String encodeTransparencyParams(List dsaParams) { + return dsaParams.stream().map(Objects::toString).collect(Collectors.joining( + TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER)); + } + private static MultiMap resolveHeaders(Site site, Device device, User user) { final MultiMap headers = MultiMap.caseInsensitiveMultiMap() .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON); @@ -339,7 +439,8 @@ private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, BidReques .dealid(resolveNumberParameter(yieldlabResponse.getPid())) .crid(makeCreativeId(bidRequest, yieldlabResponse, matchedExtImp)) .w(resolveSizeParameter(yieldlabResponse.getAdSize(), true)) - .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)); + .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)) + .ext(resolveExtParameter(yieldlabResponse)); return updatedBid; } @@ -408,4 +509,21 @@ private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, Yi yieldlabResponse.getAdSize(), uriBuilder.toString().replace("?", "")); } + + private ObjectNode resolveExtParameter(YieldlabResponse yieldlabResponse) { + final YieldlabDigitalServicesActResponse dsa = yieldlabResponse.getDsa(); + if (dsa == null) { + return null; + } + final ObjectNode ext = mapper.mapper().createObjectNode(); + final JsonNode dsaNode; + try { + dsaNode = mapper.mapper().valueToTree(dsa); + } catch (IllegalArgumentException e) { + logger.error("Failed to serialize DSA object for adslot {}", yieldlabResponse.getId(), e); + return null; + } + ext.set("dsa", dsaNode); + return ext; + } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java new file mode 100644 index 00000000000..4fd18d813df --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.yieldlab.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") +public class YieldlabDigitalServicesActResponse { + + String behalf; + + String paid; + + Integer adrender; + + List transparency; + + @AllArgsConstructor(staticName = "of") + @Value(staticConstructor = "of") + public static class Transparency { + String domain; + List dsaparams; + } +} diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java index 4cd1dbf7294..9a2b54652ad 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java @@ -22,4 +22,6 @@ public class YieldlabResponse { Integer did; String pvid; + + YieldlabDigitalServicesActResponse dsa; } diff --git a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java index 194ce9308db..872457921ee 100644 --- a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java @@ -15,7 +15,9 @@ import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.bidder.yieldmo.proto.YieldmoBidExt; import org.prebid.server.bidder.yieldmo.proto.YieldmoImpExt; import org.prebid.server.exception.PreBidException; @@ -27,6 +29,7 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -39,12 +42,17 @@ public class YieldmoBidder implements Bidder { private static final TypeReference> YIELDMO_EXT_TYPE_REFERENCE = new TypeReference<>() { }; + private static final String USD_CURRENCY = "USD"; private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public YieldmoBidder(String endpointUrl, JacksonMapper mapper) { + public YieldmoBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -56,7 +64,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { final ExtImpYieldmo impExt = parseImpExt(imp); - modifiedImps.add(modifyImp(imp, impExt)); + modifiedImps.add(modifyImp(imp, bidRequest, impExt)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -78,9 +86,31 @@ private ExtImpYieldmo parseImpExt(Imp imp) throws PreBidException { } } - private Imp modifyImp(Imp imp, ExtImpYieldmo ext) { + private Imp modifyImp(Imp imp, BidRequest bidRequest, ExtImpYieldmo ext) { final YieldmoImpExt modifiedExt = YieldmoImpExt.of(ext.getPlacementId(), extractGpid(imp)); - return imp.toBuilder().ext(mapper.mapper().valueToTree(modifiedExt)).build(); + + Price bidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + bidFloorPrice = BidderUtil.isValidPrice(bidFloorPrice) + ? convertBidFloor(bidFloorPrice, imp.getId(), bidRequest) : bidFloorPrice; + + return imp.toBuilder() + .bidfloor(bidFloorPrice.getValue()) + .bidfloorcur(bidFloorPrice.getCurrency()) + .ext(mapper.mapper().valueToTree(modifiedExt)) + .build(); + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { + final String bidFloorCur = bidFloorPrice.getCurrency(); + try { + final BigDecimal convertedPrice = currencyConversionService + .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, USD_CURRENCY); + + return Price.of(USD_CURRENCY, convertedPrice); + } catch (PreBidException e) { + // If currency conversion fails, we still want to receive the bid request. + return bidFloorPrice; + } } private static String extractGpid(Imp imp) { diff --git a/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java b/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java index 4e4744b06fb..cb5ae6968b5 100644 --- a/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java @@ -69,7 +69,7 @@ private Imp modifyImp(Imp imp) { final Banner banner = imp.getBanner(); if (banner != null) { if (banner.getH() == null && banner.getW() == null && CollectionUtils.isNotEmpty(banner.getFormat())) { - final Format firstFormat = banner.getFormat().get(0); + final Format firstFormat = banner.getFormat().getFirst(); final Banner modifiedBanner = banner.toBuilder() .h(firstFormat.getH()) .w(firstFormat.getW()) diff --git a/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java b/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java deleted file mode 100644 index 804071d0a2b..00000000000 --- a/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.prebid.server.bidder.zeta_global_ssp; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class ZetaGlobalSspBidder implements Bidder { - - private final String endpointUrl; - private final JacksonMapper mapper; - - public ZetaGlobalSspBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest bidRequest) { - return Result.withValue( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(HttpUtil.headers()) - .body(mapper.encodeToBytes(bidRequest)) - .impIds(BidderUtil.impIds(bidRequest)) - .payload(bidRequest) - .build()); - } - - @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); - } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - return bidsFromResponse(bidRequest, bidResponse); - } - - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, getBidType(bid, bidRequest.getImp()), bidResponse.getCur())) - .toList(); - } - - private static BidType getBidType(Bid bid, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(bid.getImpid())) { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } - } - } - return BidType.banner; - } -} diff --git a/src/main/java/org/prebid/server/bidder/zmaticoo/ZMaticooBidder.java b/src/main/java/org/prebid/server/bidder/zmaticoo/ZMaticooBidder.java new file mode 100644 index 00000000000..028d3dbc6de --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/zmaticoo/ZMaticooBidder.java @@ -0,0 +1,182 @@ +package org.prebid.server.bidder.zmaticoo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.UpdateResult; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.zmaticoo.ExtImpZMaticoo; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ZMaticooBidder implements Bidder { + + private static final TypeReference> ZMATICOO_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ZMaticooBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List bidderErrors = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + validateImpExt(imp); + modifiedImps.add(modifyImp(imp)); + } catch (PreBidException e) { + bidderErrors.add(BidderError.badInput(e.getMessage())); + } + } + + if (CollectionUtils.isNotEmpty(bidderErrors)) { + return Result.withErrors(bidderErrors); + } + + final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); + return Result.withValue(makeHttpRequest(modifiedRequest)); + } + + private void validateImpExt(Imp imp) { + final ExtImpZMaticoo extImpZMaticoo; + try { + extImpZMaticoo = mapper.mapper().convertValue(imp.getExt(), ZMATICOO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + if (StringUtils.isBlank(extImpZMaticoo.getPubId()) || StringUtils.isBlank(extImpZMaticoo.getZoneId())) { + throw new PreBidException("imp.ext.pubId or imp.ext.zoneId required"); + } + } + + private Imp modifyImp(Imp imp) { + final Native xNative = imp.getXNative(); + if (xNative == null) { + return imp; + } + + final UpdateResult nativeRequest = resolveNativeRequest(xNative.getRequest()); + return nativeRequest.isUpdated() + ? imp.toBuilder() + .xNative(xNative.toBuilder() + .request(nativeRequest.getValue()) + .build()) + .build() + : imp; + } + + private UpdateResult resolveNativeRequest(String nativeRequest) { + final JsonNode nativeRequestNode; + try { + nativeRequestNode = StringUtils.isNotBlank(nativeRequest) + ? mapper.mapper().readTree(nativeRequest) + : mapper.mapper().createObjectNode(); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + + if (nativeRequestNode.has("native")) { + return UpdateResult.unaltered(nativeRequest); + } + + final String updatedNativeRequest = mapper.mapper().createObjectNode() + .putPOJO("native", nativeRequestNode) + .toString(); + + return UpdateResult.updated(updatedNativeRequest); + } + + private HttpRequest makeHttpRequest(BidRequest modifiedRequest) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(modifiedRequest)) + .payload(modifiedRequest) + .body(mapper.encodeToBytes(modifiedRequest)) + .build(); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + try { + final BidType bidType = getBidMediaType(bid); + return BidderBid.of(bid, bidType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidMediaType(Bid bid) { + final int markupType = ObjectUtils.defaultIfNull(bid.getMtype(), 0); + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "unrecognized bid type in response from zmaticoo for bid " + bid.getImpid()); + }; + } + +} diff --git a/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java new file mode 100644 index 00000000000..956330cd1ca --- /dev/null +++ b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java @@ -0,0 +1,194 @@ +package org.prebid.server.cache; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.cache.proto.request.module.ModuleCacheRequest; +import org.prebid.server.cache.proto.request.module.StorageDataType; +import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; +import org.prebid.server.cache.utils.CacheServiceUtil; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.HttpClient; + +import java.net.URL; +import java.util.Objects; + +public class BasicPbcStorageService implements PbcStorageService { + + public static final String MODULE_KEY_PREFIX = "module"; + public static final String MODULE_KEY_DELIMETER = "."; + + private final HttpClient httpClient; + private final URL endpointUrl; + private final String apiKey; + private final int callTimeoutMs; + private final JacksonMapper mapper; + + public BasicPbcStorageService(HttpClient httpClient, + URL endpointUrl, + String apiKey, + int callTimeoutMs, + JacksonMapper mapper) { + + this.httpClient = Objects.requireNonNull(httpClient); + this.endpointUrl = Objects.requireNonNull(endpointUrl); + this.apiKey = Objects.requireNonNull(apiKey); + this.callTimeoutMs = callTimeoutMs; + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future storeEntry(String key, + String value, + StorageDataType type, + Integer ttlseconds, + String application, + String appCode) { + + try { + validateStoreData(key, value, application, type, appCode); + } catch (PreBidException e) { + return Future.failedFuture(e); + } + + final ModuleCacheRequest moduleCacheRequest = + ModuleCacheRequest.of( + constructEntryKey(key, appCode), + type, + prepareValueForStoring(value, type), + application, + ttlseconds); + + return httpClient.post( + endpointUrl.toString(), + securedCallHeaders(), + mapper.encodeToString(moduleCacheRequest), + callTimeoutMs) + .compose(response -> processStoreResponse(response.getStatusCode(), response.getBody())); + + } + + private static void validateStoreData(String key, + String value, + String application, + StorageDataType type, + String moduleCode) { + + if (StringUtils.isBlank(key)) { + throw new PreBidException("Module cache 'key' can not be blank"); + } + + if (StringUtils.isBlank(value)) { + throw new PreBidException("Module cache 'value' can not be blank"); + } + + if (StringUtils.isBlank(application)) { + throw new PreBidException("Module cache 'application' can not be blank"); + } + + if (type == null) { + throw new PreBidException("Module cache 'type' can not be empty"); + } + + if (StringUtils.isBlank(moduleCode)) { + throw new PreBidException("Module cache 'moduleCode' can not be blank"); + } + } + + private static String prepareValueForStoring(String value, StorageDataType type) { + return type == StorageDataType.TEXT + ? new String(Base64.encodeBase64(value.getBytes())) + : value; + } + + private MultiMap securedCallHeaders() { + return CacheServiceUtil.CACHE_HEADERS + .add(HttpUtil.X_PBC_API_KEY_HEADER, apiKey); + } + + private String constructEntryKey(String key, String moduleCode) { + return MODULE_KEY_PREFIX + MODULE_KEY_DELIMETER + moduleCode + MODULE_KEY_DELIMETER + key; + } + + private Future processStoreResponse(int statusCode, String responseBody) { + if (statusCode != 204) { + throw new PreBidException("HTTP status code: '%s', body: '%s' " + .formatted(statusCode, responseBody)); + } + + return Future.succeededFuture(); + } + + @Override + public Future retrieveEntry(String key, + String appCode, + String application) { + + try { + validateRetrieveData(key, application, appCode); + } catch (PreBidException e) { + return Future.failedFuture(e); + } + + return httpClient.get( + getRetrieveEndpoint(key, appCode, application), + securedCallHeaders(), + callTimeoutMs) + .map(response -> toModuleCacheResponse(response.getStatusCode(), response.getBody())); + + } + + private static void validateRetrieveData(String key, String application, String moduleCode) { + if (StringUtils.isBlank(key)) { + throw new PreBidException("Module cache 'key' can not be blank"); + } + + if (StringUtils.isBlank(application)) { + throw new PreBidException("Module cache 'application' can not be blank"); + } + + if (StringUtils.isBlank(moduleCode)) { + throw new PreBidException("Module cache 'moduleCode' can not be blank"); + } + } + + private String getRetrieveEndpoint(String key, + String moduleCode, + String application) { + + return endpointUrl + + "?k=" + constructEntryKey(key, moduleCode) + + "&a=" + application; + } + + private ModuleCacheResponse toModuleCacheResponse(int statusCode, String responseBody) { + if (statusCode != 200) { + throw new PreBidException("HTTP status code " + statusCode); + } + + final ModuleCacheResponse moduleCacheResponse; + try { + moduleCacheResponse = mapper.decodeValue(responseBody, ModuleCacheResponse.class); + } catch (DecodeException e) { + throw new PreBidException("Cannot parse response: " + responseBody, e); + } + + final String processedValue = + prepareValueAfterRetrieve(moduleCacheResponse.getValue(), moduleCacheResponse.getType()); + + // Use == instead of equals, because it is enough to check if the reference has changed + return moduleCacheResponse.getValue() == processedValue + ? moduleCacheResponse + : ModuleCacheResponse.of(moduleCacheResponse.getKey(), moduleCacheResponse.getType(), processedValue); + } + + private static String prepareValueAfterRetrieve(String value, StorageDataType type) { + return type == StorageDataType.TEXT + ? new String(Base64.decodeBase64(value.getBytes())) + : value; + } +} diff --git a/src/main/java/org/prebid/server/cache/CacheService.java b/src/main/java/org/prebid/server/cache/CacheService.java deleted file mode 100644 index 255144d0df9..00000000000 --- a/src/main/java/org/prebid/server/cache/CacheService.java +++ /dev/null @@ -1,716 +0,0 @@ -package org.prebid.server.cache; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import lombok.Value; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.model.BidInfo; -import org.prebid.server.auction.model.CachedDebugLog; -import org.prebid.server.cache.model.CacheBid; -import org.prebid.server.cache.model.CacheContext; -import org.prebid.server.cache.model.CacheHttpRequest; -import org.prebid.server.cache.model.CacheHttpResponse; -import org.prebid.server.cache.model.CacheInfo; -import org.prebid.server.cache.model.CacheServiceResult; -import org.prebid.server.cache.model.CacheTtl; -import org.prebid.server.cache.model.DebugHttpCall; -import org.prebid.server.cache.proto.request.BidCacheRequest; -import org.prebid.server.cache.proto.request.PutObject; -import org.prebid.server.cache.proto.response.BidCacheResponse; -import org.prebid.server.cache.proto.response.CacheObject; -import org.prebid.server.events.EventsContext; -import org.prebid.server.events.EventsService; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.identity.UUIDIdGenerator; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.metric.Metrics; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAuctionConfig; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; -import org.prebid.server.vast.VastModifier; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Clock; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Client stores values in Prebid Cache. - *

- * For more info, see https://github.com/prebid/prebid-cache project. - */ -public class CacheService { - - private static final Logger logger = LoggerFactory.getLogger(CacheService.class); - - private static final MultiMap CACHE_HEADERS = HttpUtil.headers(); - private static final Map> DEBUG_HEADERS = HttpUtil.toDebugHeaders(CACHE_HEADERS); - private static final String BID_WURL_ATTRIBUTE = "wurl"; - private static final String XML_CREATIVE_TYPE = "xml"; - private static final String JSON_CREATIVE_TYPE = "json"; - - private final CacheTtl mediaTypeCacheTtl; - private final HttpClient httpClient; - private final URL endpointUrl; - private final String cachedAssetUrlTemplate; - private final long expectedCacheTimeMs; - private final VastModifier vastModifier; - private final EventsService eventsService; - private final Metrics metrics; - private final Clock clock; - private final UUIDIdGenerator idGenerator; - private final JacksonMapper mapper; - - public CacheService(CacheTtl mediaTypeCacheTtl, - HttpClient httpClient, - URL endpointUrl, - String cachedAssetUrlTemplate, - long expectedCacheTimeMs, - VastModifier vastModifier, - EventsService eventsService, - Metrics metrics, - Clock clock, - UUIDIdGenerator idGenerator, - JacksonMapper mapper) { - - this.mediaTypeCacheTtl = Objects.requireNonNull(mediaTypeCacheTtl); - this.httpClient = Objects.requireNonNull(httpClient); - this.endpointUrl = Objects.requireNonNull(endpointUrl); - this.cachedAssetUrlTemplate = Objects.requireNonNull(cachedAssetUrlTemplate); - this.expectedCacheTimeMs = expectedCacheTimeMs; - this.vastModifier = Objects.requireNonNull(vastModifier); - this.eventsService = Objects.requireNonNull(eventsService); - this.metrics = Objects.requireNonNull(metrics); - this.clock = Objects.requireNonNull(clock); - this.idGenerator = Objects.requireNonNull(idGenerator); - this.mapper = Objects.requireNonNull(mapper); - } - - public String getEndpointHost() { - final String host = endpointUrl.getHost(); - final int port = endpointUrl.getPort(); - return port != -1 ? "%s:%d".formatted(host, port) : host; - } - - public String getEndpointPath() { - return endpointUrl.getPath(); - } - - public String getCachedAssetURLTemplate() { - return cachedAssetUrlTemplate; - } - - /** - * Makes cache for debugLog only and returns generated cache object key without wait for result. - */ - public String cacheVideoDebugLog(CachedDebugLog cachedDebugLog, Integer videoCacheTtl) { - final String cacheKey = cachedDebugLog.getCacheKey() == null - ? idGenerator.generateId() - : cachedDebugLog.getCacheKey(); - final List cachedCreatives = Collections.singletonList( - makeDebugCacheCreative(cachedDebugLog, cacheKey, videoCacheTtl)); - final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); - httpClient.post(endpointUrl.toString(), HttpUtil.headers(), mapper.encodeToString(bidCacheRequest), - expectedCacheTimeMs); - return cacheKey; - } - - private CachedCreative makeDebugCacheCreative(CachedDebugLog videoCacheDebugLog, String hbCacheId, - Integer videoCacheTtl) { - final JsonNode value = mapper.mapper().valueToTree(videoCacheDebugLog.buildCacheBody()); - videoCacheDebugLog.setCacheKey(hbCacheId); - return CachedCreative.of(PutObject.builder() - .type(CachedDebugLog.CACHE_TYPE) - .value(new TextNode(videoCacheDebugLog.buildCacheBody())) - .expiry(videoCacheTtl != null ? videoCacheTtl : videoCacheDebugLog.getTtl()) - .key("log_" + hbCacheId) - .build(), creativeSizeFromTextNode(value)); - } - - /** - * Asks external prebid cache service to store the given value. - */ - private Future makeRequest(BidCacheRequest bidCacheRequest, - int bidCount, - Timeout timeout, - String accountId) { - - if (bidCount == 0) { - return Future.succeededFuture(BidCacheResponse.of(Collections.emptyList())); - } - - final long remainingTimeout = timeout.remaining(); - if (remainingTimeout <= 0) { - return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); - } - - final long startTime = clock.millis(); - return httpClient.post(endpointUrl.toString(), CACHE_HEADERS, mapper.encodeToString(bidCacheRequest), - remainingTimeout) - .map(response -> toBidCacheResponse( - response.getStatusCode(), response.getBody(), bidCount, accountId, startTime)) - .recover(exception -> failResponse(exception, accountId, startTime)); - } - - /** - * Handles errors occurred while HTTP request or response processing. - */ - private Future failResponse(Throwable exception, String accountId, long startTime) { - metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); - - logger.warn("Error occurred while interacting with cache service: {0}", exception.getMessage()); - logger.debug("Error occurred while interacting with cache service", exception); - - return Future.failedFuture(exception); - } - - /** - * Makes cache for Vtrack puts. - *

- * Modify VAST value in putObjects and stores in the cache. - *

- * The returned result will always have the number of elements equals putObjects list size. - */ - public Future cachePutObjects(List putObjects, - Boolean isEventsEnabled, - Set biddersAllowingVastUpdate, - String accountId, - String integration, - Timeout timeout) { - - final List cachedCreatives = - updatePutObjects(putObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, integration); - - updateCreativeMetrics(accountId, cachedCreatives); - - return makeRequest(toBidCacheRequest(cachedCreatives), cachedCreatives.size(), timeout, accountId); - } - - /** - * Modify VAST value in putObjects. - */ - private List updatePutObjects(List putObjects, - Boolean isEventsEnabled, - Set allowedBidders, - String accountId, - String integration) { - - return putObjects.stream() - .map(putObject -> putObject.toBuilder() - // remove "/vtrack" specific fields - .bidid(null) - .bidder(null) - .timestamp(null) - .value(vastModifier.modifyVastXml(isEventsEnabled, - allowedBidders, - putObject, - accountId, - integration)) - .build()) - .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) - .toList(); - } - - public Future cacheBidsOpenrtb(List bidsToCache, - AuctionContext auctionContext, - CacheContext cacheContext, - EventsContext eventsContext) { - - if (CollectionUtils.isEmpty(bidsToCache)) { - return Future.succeededFuture(CacheServiceResult.empty()); - } - - final List imps = auctionContext.getBidRequest().getImp(); - final boolean isAnyEmptyExpImp = imps.stream() - .map(Imp::getExp) - .anyMatch(Objects::isNull); - - final Account account = auctionContext.getAccount(); - final CacheTtl accountCacheTtl = accountCacheTtl(isAnyEmptyExpImp, account); - - final List cacheBids = cacheContext.isShouldCacheBids() - ? getCacheBids(bidsToCache, cacheContext.getCacheBidsTtl(), accountCacheTtl) - : Collections.emptyList(); - - final List videoCacheBids = cacheContext.isShouldCacheVideoBids() - ? getVideoCacheBids(bidsToCache, cacheContext.getCacheVideoBidsTtl(), accountCacheTtl) - : Collections.emptyList(); - - return doCacheOpenrtb(cacheBids, videoCacheBids, auctionContext, eventsContext); - } - - /** - * Fetches {@link CacheTtl} from {@link Account}. - *

- * Returns empty {@link CacheTtl} when there are no impressions without expiration or - * if {@link Account} has neither of banner or video cache ttl. - */ - private CacheTtl accountCacheTtl(boolean impWithNoExpExists, Account account) { - final AccountAuctionConfig accountAuctionConfig = account.getAuction(); - final Integer bannerCacheTtl = accountAuctionConfig != null ? accountAuctionConfig.getBannerCacheTtl() : null; - final Integer videoCacheTtl = accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null; - - return impWithNoExpExists && (bannerCacheTtl != null || videoCacheTtl != null) - ? CacheTtl.of(bannerCacheTtl, videoCacheTtl) - : CacheTtl.empty(); - } - - private List getCacheBids(List bidInfos, - Integer cacheBidsTtl, - CacheTtl accountCacheTtl) { - - return bidInfos.stream() - .map(bidInfo -> toCacheBid(bidInfo, cacheBidsTtl, accountCacheTtl, false)) - .toList(); - } - - private List getVideoCacheBids(List bidInfos, - Integer cacheBidsTtl, - CacheTtl accountCacheTtl) { - - return bidInfos.stream() - .filter(bidInfo -> Objects.equals(bidInfo.getBidType(), BidType.video)) - .map(bidInfo -> toCacheBid(bidInfo, cacheBidsTtl, accountCacheTtl, true)) - .toList(); - } - - /** - * Creates {@link CacheBid} from given {@link BidInfo} and determined cache ttl. - */ - private CacheBid toCacheBid(BidInfo bidInfo, - Integer requestTtl, - CacheTtl accountCacheTtl, - boolean isVideoBid) { - - final Bid bid = bidInfo.getBid(); - final Integer bidTtl = bid.getExp(); - final Imp correspondingImp = bidInfo.getCorrespondingImp(); - final Integer impTtl = correspondingImp != null ? correspondingImp.getExp() : null; - final Integer accountMediaTypeTtl = isVideoBid - ? accountCacheTtl.getVideoCacheTtl() - : accountCacheTtl.getBannerCacheTtl(); - final Integer mediaTypeTtl = isVideoBid - ? mediaTypeCacheTtl.getVideoCacheTtl() - : mediaTypeCacheTtl.getBannerCacheTtl(); - final Integer ttl = ObjectUtils.firstNonNull(bidTtl, impTtl, requestTtl, accountMediaTypeTtl, mediaTypeTtl); - - return CacheBid.of(bidInfo, ttl); - } - - /** - * Makes cache for OpenRTB bids. - *

- * Stores JSON values for the given {@link com.iab.openrtb.response.Bid}s in the cache. - * Stores XML cache objects for the given video {@link com.iab.openrtb.response.Bid}s in the cache. - *

- * The returned result will always have the number of elements equals to sum of sizes of bids and video bids. - */ - private Future doCacheOpenrtb(List bids, - List videoBids, - AuctionContext auctionContext, - EventsContext eventsContext) { - - final Account account = auctionContext.getAccount(); - final String accountId = account.getId(); - final String hbCacheId = videoBids.stream().anyMatch(cacheBid -> cacheBid.getBidInfo().getCategory() != null) - ? idGenerator.generateId() - : null; - final String requestId = auctionContext.getBidRequest().getId(); - final List cachedCreatives = Stream.concat( - bids.stream().map(cacheBid -> - createJsonPutObjectOpenrtb(cacheBid, accountId, eventsContext)), - videoBids.stream().map(videoBid -> createXmlPutObjectOpenrtb(videoBid, requestId, hbCacheId))) - .collect(Collectors.toCollection(ArrayList::new)); - - if (cachedCreatives.isEmpty()) { - return Future.succeededFuture(CacheServiceResult.empty()); - } - - final CachedDebugLog cachedDebugLog = auctionContext.getCachedDebugLog(); - - final Integer videoCacheTtl = ObjectUtil.getIfNotNull(account.getAuction(), - AccountAuctionConfig::getVideoCacheTtl); - if (CollectionUtils.isNotEmpty(cachedCreatives) && cachedDebugLog != null && cachedDebugLog.isEnabled()) { - cachedCreatives.add(makeDebugCacheCreative(cachedDebugLog, hbCacheId, videoCacheTtl)); - } - - final long remainingTimeout = auctionContext.getTimeoutContext().getTimeout().remaining(); - if (remainingTimeout <= 0) { - return Future.succeededFuture(CacheServiceResult.of(null, new TimeoutException("Timeout has been exceeded"), - Collections.emptyMap())); - } - - final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); - - updateCreativeMetrics(accountId, cachedCreatives); - - final String url = endpointUrl.toString(); - final String body = mapper.encodeToString(bidCacheRequest); - final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body); - - final long startTime = clock.millis(); - return httpClient.post(url, CACHE_HEADERS, body, remainingTimeout) - .map(response -> processResponseOpenrtb(response, - httpRequest, - cachedCreatives.size(), - bids, - videoBids, - hbCacheId, - accountId, - startTime)) - .otherwise(exception -> failResponseOpenrtb(exception, accountId, httpRequest, startTime)); - } - - /** - * Creates {@link CacheServiceResult} from the given {@link HttpClientResponse}. - */ - private CacheServiceResult processResponseOpenrtb(HttpClientResponse response, - CacheHttpRequest httpRequest, - int bidCount, - List bids, - List videoBids, - String hbCacheId, - String accountId, - long startTime) { - - final CacheHttpResponse httpResponse = CacheHttpResponse.of(response.getStatusCode(), response.getBody()); - final int responseStatusCode = response.getStatusCode(); - final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), httpRequest, httpResponse, startTime); - final BidCacheResponse bidCacheResponse; - try { - bidCacheResponse = toBidCacheResponse( - responseStatusCode, response.getBody(), bidCount, accountId, startTime); - } catch (PreBidException e) { - return CacheServiceResult.of(httpCall, e, Collections.emptyMap()); - } - - final List uuids = toResponse(bidCacheResponse, CacheObject::getUuid); - return CacheServiceResult.of(httpCall, null, toResultMap(bids, videoBids, uuids, hbCacheId)); - } - - /** - * Handles errors occurred while HTTP request or response processing. - */ - private CacheServiceResult failResponseOpenrtb(Throwable exception, - String accountId, - CacheHttpRequest request, - long startTime) { - - logger.warn("Error occurred while interacting with cache service: {0}", exception.getMessage()); - logger.debug("Error occurred while interacting with cache service", exception); - - metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); - - final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), request, null, startTime); - return CacheServiceResult.of(httpCall, exception, Collections.emptyMap()); - } - - /** - * Creates {@link DebugHttpCall} from {@link CacheHttpRequest} and {@link CacheHttpResponse}, endpoint - * and starttime. - */ - private DebugHttpCall makeDebugHttpCall(String endpoint, - CacheHttpRequest httpRequest, - CacheHttpResponse httpResponse, - long startTime) { - - return DebugHttpCall.builder() - .endpoint(endpoint) - .requestUri(httpRequest != null ? httpRequest.getUri() : null) - .requestBody(httpRequest != null ? httpRequest.getBody() : null) - .responseStatus(httpResponse != null ? httpResponse.getStatusCode() : null) - .responseBody(httpResponse != null ? httpResponse.getBody() : null) - .responseTimeMillis(responseTime(startTime)) - .requestHeaders(DEBUG_HEADERS) - .build(); - } - - /** - * Calculates execution time since the given start time. - */ - private int responseTime(long startTime) { - return Math.toIntExact(clock.millis() - startTime); - } - - /** - * Makes JSON type {@link PutObject} from {@link com.iab.openrtb.response.Bid}. - * Used for OpenRTB auction request. Also, adds win url to result object if events are enabled. - */ - private CachedCreative createJsonPutObjectOpenrtb(CacheBid cacheBid, - String accountId, - EventsContext eventsContext) { - - final BidInfo bidInfo = cacheBid.getBidInfo(); - final Bid bid = bidInfo.getBid(); - final ObjectNode bidObjectNode = mapper.mapper().valueToTree(bid); - - final String eventUrl = - generateWinUrl(bidInfo.getBidId(), - bidInfo.getBidder(), - accountId, - eventsContext, - bidInfo.getLineItemId()); - if (eventUrl != null) { - bidObjectNode.put(BID_WURL_ATTRIBUTE, eventUrl); - } - - final PutObject payload = PutObject.builder() - .aid(eventsContext.getAuctionId()) - .type("json") - .value(bidObjectNode) - .ttlseconds(cacheBid.getTtl()) - .build(); - - return CachedCreative.of(payload, creativeSizeFromAdm(bid.getAdm())); - } - - /** - * Makes XML type {@link PutObject} from {@link com.iab.openrtb.response.Bid}. Used for OpenRTB auction request. - */ - private CachedCreative createXmlPutObjectOpenrtb(CacheBid cacheBid, String requestId, String hbCacheId) { - final BidInfo bidInfo = cacheBid.getBidInfo(); - final Bid bid = bidInfo.getBid(); - final String vastXml = bid.getAdm(); - - final String customCacheKey = resolveCustomCacheKey(hbCacheId, bidInfo.getCategory()); - - final PutObject payload = PutObject.builder() - .aid(requestId) - .type("xml") - .value(vastXml != null ? new TextNode(vastXml) : null) - .ttlseconds(cacheBid.getTtl()) - .key(customCacheKey) - .build(); - - return CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue())); - } - - private static String resolveCustomCacheKey(String hbCacheId, String category) { - return StringUtils.isNoneEmpty(category, hbCacheId) - ? "%s_%s".formatted(category, hbCacheId) - : null; - } - - private String generateWinUrl(String bidId, - String bidder, - String accountId, - EventsContext eventsContext, - String lineItemId) { - - if (!eventsContext.isEnabledForAccount()) { - return null; - } - - if (eventsContext.isEnabledForRequest() || StringUtils.isNotBlank(lineItemId)) { - return eventsService.winUrl( - bidId, - bidder, - accountId, - lineItemId, - eventsContext.isEnabledForRequest(), - eventsContext); - } - - return null; - } - - /** - * Handles http response, analyzes response status and creates {@link BidCacheResponse} from response body - * or throws {@link PreBidException} in case of errors. - */ - private BidCacheResponse toBidCacheResponse(int statusCode, - String responseBody, - int bidCount, - String accountId, - long startTime) { - - if (statusCode != 200) { - throw new PreBidException("HTTP status code " + statusCode); - } - - final BidCacheResponse bidCacheResponse; - try { - bidCacheResponse = mapper.decodeValue(responseBody, BidCacheResponse.class); - } catch (DecodeException e) { - throw new PreBidException("Cannot parse response: " + responseBody, e); - } - - final List responses = bidCacheResponse.getResponses(); - if (responses == null || responses.size() != bidCount) { - throw new PreBidException("The number of response cache objects doesn't match with bids"); - } - - metrics.updateCacheRequestSuccessTime(accountId, clock.millis() - startTime); - return bidCacheResponse; - } - - /** - * Creates prebid cache service response according to the creator. - */ - private List toResponse(BidCacheResponse bidCacheResponse, Function responseItemCreator) { - return bidCacheResponse.getResponses().stream() - .filter(Objects::nonNull) - .map(responseItemCreator) - .filter(Objects::nonNull) - .toList(); - } - - /** - * Creates a map with bids as a key and {@link CacheInfo} as a value from obtained UUIDs. - */ - private static Map toResultMap(List cacheBids, - List cacheVideoBids, - List uuids, - String hbCacheId) { - - final Map result = new HashMap<>(uuids.size()); - - // here we assume "videoBids" is a sublist of "bids" - // so, no need for a separate loop on "videoBids" if "bids" is not empty - if (!cacheBids.isEmpty()) { - final List videoBids = cacheVideoBids.stream() - .map(CacheBid::getBidInfo) - .map(BidInfo::getBid) - .toList(); - - final int bidsSize = cacheBids.size(); - for (int i = 0; i < bidsSize; i++) { - final CacheBid cacheBid = cacheBids.get(i); - final BidInfo bidInfo = cacheBid.getBidInfo(); - final Bid bid = bidInfo.getBid(); - final Integer ttl = cacheBid.getTtl(); - - // determine uuid for video bid - final int indexOfVideoBid = videoBids.indexOf(bid); - final String videoBidUuid = indexOfVideoBid != -1 ? uuids.get(bidsSize + indexOfVideoBid) : null; - final Integer videoTtl = indexOfVideoBid != -1 ? cacheVideoBids.get(indexOfVideoBid).getTtl() : null; - - result.put(bid, CacheInfo.of(uuids.get(i), resolveVideoBidUuid(videoBidUuid, hbCacheId), ttl, - videoTtl)); - } - } else { - for (int i = 0; i < cacheVideoBids.size(); i++) { - final CacheBid cacheBid = cacheVideoBids.get(i); - final BidInfo bidInfo = cacheBid.getBidInfo(); - result.put(bidInfo.getBid(), CacheInfo.of(null, resolveVideoBidUuid(uuids.get(i), hbCacheId), null, - cacheBid.getTtl())); - } - } - - return result; - } - - private static String resolveVideoBidUuid(String uuid, String hbCacheId) { - return hbCacheId != null && uuid.endsWith(hbCacheId) ? hbCacheId : uuid; - } - - /** - * Composes prebid cache service url against the given schema and host. - */ - public static URL getCacheEndpointUrl(String cacheSchema, String cacheHost, String path) { - try { - final URL baseUrl = getCacheBaseUrl(cacheSchema, cacheHost); - return new URL(baseUrl, path); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Could not get cache endpoint for prebid cache service", e); - } - } - - /** - * Composes cached asset url template against the given query, schema and host. - */ - public static String getCachedAssetUrlTemplate(String cacheSchema, - String cacheHost, - String path, - String cacheQuery) { - - try { - final URL baseUrl = getCacheBaseUrl(cacheSchema, cacheHost); - return new URL(baseUrl, path + "?" + cacheQuery).toString(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Could not get cached asset url template for prebid cache service", e); - } - } - - /** - * Returns prebid cache service url or throws {@link MalformedURLException} if error occurs. - */ - private static URL getCacheBaseUrl(String cacheSchema, String cacheHost) throws MalformedURLException { - return new URL(cacheSchema + "://" + cacheHost); - } - - private void updateCreativeMetrics(String accountId, List cachedCreatives) { - for (final CachedCreative cachedCreative : cachedCreatives) { - metrics.updateCacheCreativeSize(accountId, - cachedCreative.getSize(), - resolveCreativeTypeName(cachedCreative.getPayload())); - } - } - - private static MetricName resolveCreativeTypeName(PutObject putObject) { - final String typeValue = ObjectUtil.getIfNotNull(putObject, PutObject::getType); - - if (Objects.equals(typeValue, XML_CREATIVE_TYPE)) { - return MetricName.xml; - } - - if (Objects.equals(typeValue, JSON_CREATIVE_TYPE)) { - return MetricName.json; - } - - return MetricName.unknown; - } - - private static int creativeSizeFromAdm(String adm) { - return lengthOrZero(adm); - } - - private static int lengthOrZero(String adm) { - return adm != null ? adm.length() : 0; - } - - private static int creativeSizeFromTextNode(JsonNode node) { - return node != null ? node.asText().length() : 0; - } - - private BidCacheRequest toBidCacheRequest(List cachedCreatives) { - return BidCacheRequest.of(cachedCreatives.stream() - .map(CachedCreative::getPayload) - .toList()); - } - - @Value(staticConstructor = "of") - private static class CachedCreative { - - PutObject payload; - - int size; - } -} diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java new file mode 100644 index 00000000000..5d5034e23ce --- /dev/null +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -0,0 +1,556 @@ +package org.prebid.server.cache; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.response.Bid; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidInfo; +import org.prebid.server.auction.model.CachedDebugLog; +import org.prebid.server.cache.model.CacheBid; +import org.prebid.server.cache.model.CacheContext; +import org.prebid.server.cache.model.CacheHttpRequest; +import org.prebid.server.cache.model.CacheHttpResponse; +import org.prebid.server.cache.model.CacheInfo; +import org.prebid.server.cache.model.CacheServiceResult; +import org.prebid.server.cache.model.CachedCreative; +import org.prebid.server.cache.model.DebugHttpCall; +import org.prebid.server.cache.proto.request.bid.BidCacheRequest; +import org.prebid.server.cache.proto.request.bid.BidPutObject; +import org.prebid.server.cache.proto.response.bid.BidCacheResponse; +import org.prebid.server.cache.proto.response.bid.CacheObject; +import org.prebid.server.cache.utils.CacheServiceUtil; +import org.prebid.server.events.EventsContext; +import org.prebid.server.events.EventsService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.identity.UUIDIdGenerator; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.vast.VastModifier; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.net.URL; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CoreCacheService { + + private static final Logger logger = LoggerFactory.getLogger(CoreCacheService.class); + + private static final String BID_WURL_ATTRIBUTE = "wurl"; + + private final HttpClient httpClient; + private final URL endpointUrl; + private final String cachedAssetUrlTemplate; + private final long expectedCacheTimeMs; + private final VastModifier vastModifier; + private final EventsService eventsService; + private final Metrics metrics; + private final Clock clock; + private final UUIDIdGenerator idGenerator; + private final JacksonMapper mapper; + + private final MultiMap cacheHeaders; + private final Map> debugHeaders; + + public CoreCacheService( + HttpClient httpClient, + URL endpointUrl, + String cachedAssetUrlTemplate, + long expectedCacheTimeMs, + String apiKey, + boolean isApiKeySecured, + VastModifier vastModifier, + EventsService eventsService, + Metrics metrics, + Clock clock, + UUIDIdGenerator idGenerator, + JacksonMapper mapper) { + + this.httpClient = Objects.requireNonNull(httpClient); + this.endpointUrl = Objects.requireNonNull(endpointUrl); + this.cachedAssetUrlTemplate = Objects.requireNonNull(cachedAssetUrlTemplate); + this.expectedCacheTimeMs = expectedCacheTimeMs; + this.vastModifier = Objects.requireNonNull(vastModifier); + this.eventsService = Objects.requireNonNull(eventsService); + this.metrics = Objects.requireNonNull(metrics); + this.clock = Objects.requireNonNull(clock); + this.idGenerator = Objects.requireNonNull(idGenerator); + this.mapper = Objects.requireNonNull(mapper); + + cacheHeaders = isApiKeySecured + ? HttpUtil.headers().add(HttpUtil.X_PBC_API_KEY_HEADER, Objects.requireNonNull(apiKey)) + : HttpUtil.headers(); + debugHeaders = HttpUtil.toDebugHeaders(cacheHeaders); + } + + public String getEndpointHost() { + final String host = endpointUrl.getHost(); + final int port = endpointUrl.getPort(); + return port != -1 ? "%s:%d".formatted(host, port) : host; + } + + public String getEndpointPath() { + return endpointUrl.getPath(); + } + + public String getCachedAssetURLTemplate() { + return cachedAssetUrlTemplate; + } + + public String cacheVideoDebugLog(CachedDebugLog cachedDebugLog, Integer videoCacheTtl) { + final String cacheKey = cachedDebugLog.getCacheKey() == null + ? idGenerator.generateId() + : cachedDebugLog.getCacheKey(); + final List cachedCreatives = Collections.singletonList( + makeDebugCacheCreative(cachedDebugLog, cacheKey, videoCacheTtl)); + final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); + httpClient.post( + endpointUrl.toString(), + cacheHeaders, + mapper.encodeToString(bidCacheRequest), + expectedCacheTimeMs); + return cacheKey; + } + + private CachedCreative makeDebugCacheCreative(CachedDebugLog videoCacheDebugLog, String hbCacheId, + Integer videoCacheTtl) { + final JsonNode value = mapper.mapper().valueToTree(videoCacheDebugLog.buildCacheBody()); + videoCacheDebugLog.setCacheKey(hbCacheId); + return CachedCreative.of(BidPutObject.builder() + .type(CachedDebugLog.CACHE_TYPE) + .value(new TextNode(videoCacheDebugLog.buildCacheBody())) + .expiry(videoCacheTtl != null ? videoCacheTtl : videoCacheDebugLog.getTtl()) + .key("log_" + hbCacheId) + .build(), creativeSizeFromTextNode(value)); + } + + private Future makeRequest(BidCacheRequest bidCacheRequest, + int bidCount, + Timeout timeout, + String accountId) { + + if (bidCount == 0) { + return Future.succeededFuture(BidCacheResponse.of(Collections.emptyList())); + } + + final long remainingTimeout = timeout.remaining(); + if (remainingTimeout <= 0) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final long startTime = clock.millis(); + return httpClient.post( + endpointUrl.toString(), + cacheHeaders, + mapper.encodeToString(bidCacheRequest), + remainingTimeout) + .map(response -> toBidCacheResponse( + response.getStatusCode(), response.getBody(), bidCount, accountId, startTime)) + .recover(exception -> failResponse(exception, accountId, startTime)); + } + + private Future failResponse(Throwable exception, String accountId, long startTime) { + metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); + + logger.warn("Error occurred while interacting with cache service: {}", exception.getMessage()); + logger.debug("Error occurred while interacting with cache service", exception); + + return Future.failedFuture(exception); + } + + public Future cachePutObjects(List bidPutObjects, + Boolean isEventsEnabled, + Set biddersAllowingVastUpdate, + String accountId, + String integration, + Timeout timeout) { + + final List cachedCreatives = + updatePutObjects(bidPutObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, integration); + + updateCreativeMetrics(accountId, cachedCreatives); + + return makeRequest(toBidCacheRequest(cachedCreatives), cachedCreatives.size(), timeout, accountId); + } + + private List updatePutObjects(List bidPutObjects, + Boolean isEventsEnabled, + Set allowedBidders, + String accountId, + String integration) { + + return bidPutObjects.stream() + .map(putObject -> putObject.toBuilder() + // remove "/vtrack" specific fields + .bidid(null) + .bidder(null) + .timestamp(null) + .value(vastModifier.modifyVastXml(isEventsEnabled, + allowedBidders, + putObject, + accountId, + integration)) + .build()) + .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) + .toList(); + } + + public Future cacheBidsOpenrtb(List bidsToCache, + AuctionContext auctionContext, + CacheContext cacheContext, + EventsContext eventsContext) { + + if (CollectionUtils.isEmpty(bidsToCache)) { + return Future.succeededFuture(CacheServiceResult.empty()); + } + + final List cacheBids = cacheContext.isShouldCacheBids() + ? getCacheBids(bidsToCache) + : Collections.emptyList(); + + final List videoCacheBids = cacheContext.isShouldCacheVideoBids() + ? getVideoCacheBids(bidsToCache) + : Collections.emptyList(); + + return doCacheOpenrtb(cacheBids, videoCacheBids, auctionContext, eventsContext); + } + + private List getCacheBids(List bidInfos) { + return bidInfos.stream() + .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getTtl())) + .toList(); + } + + private List getVideoCacheBids(List bidInfos) { + return bidInfos.stream() + .filter(bidInfo -> Objects.equals(bidInfo.getBidType(), BidType.video)) + .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVideoTtl())) + .toList(); + } + + private Future doCacheOpenrtb(List bids, + List videoBids, + AuctionContext auctionContext, + EventsContext eventsContext) { + + final Account account = auctionContext.getAccount(); + final String accountId = account.getId(); + final String hbCacheId = videoBids.stream().anyMatch(cacheBid -> cacheBid.getBidInfo().getCategory() != null) + ? idGenerator.generateId() + : null; + final String requestId = auctionContext.getBidRequest().getId(); + final List cachedCreatives = Stream.concat( + bids.stream().map(cacheBid -> + createJsonPutObjectOpenrtb(cacheBid, accountId, eventsContext)), + videoBids.stream().map(videoBid -> createXmlPutObjectOpenrtb(videoBid, requestId, hbCacheId))) + .collect(Collectors.toCollection(ArrayList::new)); + + if (cachedCreatives.isEmpty()) { + return Future.succeededFuture(CacheServiceResult.empty()); + } + + final CachedDebugLog cachedDebugLog = auctionContext.getCachedDebugLog(); + + final Integer videoCacheTtl = ObjectUtil.getIfNotNull(account.getAuction(), + AccountAuctionConfig::getVideoCacheTtl); + if (CollectionUtils.isNotEmpty(cachedCreatives) && cachedDebugLog != null && cachedDebugLog.isEnabled()) { + cachedCreatives.add(makeDebugCacheCreative(cachedDebugLog, hbCacheId, videoCacheTtl)); + } + + final long remainingTimeout = auctionContext.getTimeoutContext().getTimeout().remaining(); + if (remainingTimeout <= 0) { + return Future.succeededFuture(CacheServiceResult.of(null, new TimeoutException("Timeout has been exceeded"), + Collections.emptyMap())); + } + + final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); + + updateCreativeMetrics(accountId, cachedCreatives); + + final String url = endpointUrl.toString(); + final String body = mapper.encodeToString(bidCacheRequest); + final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body); + + final long startTime = clock.millis(); + return httpClient.post(url, cacheHeaders, body, remainingTimeout) + .map(response -> processResponseOpenrtb(response, + httpRequest, + cachedCreatives.size(), + bids, + videoBids, + hbCacheId, + accountId, + startTime)) + .otherwise(exception -> failResponseOpenrtb(exception, accountId, httpRequest, startTime)); + } + + private CacheServiceResult processResponseOpenrtb(HttpClientResponse response, + CacheHttpRequest httpRequest, + int bidCount, + List bids, + List videoBids, + String hbCacheId, + String accountId, + long startTime) { + + final CacheHttpResponse httpResponse = CacheHttpResponse.of(response.getStatusCode(), response.getBody()); + final int responseStatusCode = response.getStatusCode(); + final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), httpRequest, httpResponse, startTime); + final BidCacheResponse bidCacheResponse; + try { + bidCacheResponse = toBidCacheResponse( + responseStatusCode, response.getBody(), bidCount, accountId, startTime); + } catch (PreBidException e) { + return CacheServiceResult.of(httpCall, e, Collections.emptyMap()); + } + + final List uuids = toResponse(bidCacheResponse, CacheObject::getUuid); + return CacheServiceResult.of(httpCall, null, toResultMap(bids, videoBids, uuids, hbCacheId)); + } + + private CacheServiceResult failResponseOpenrtb(Throwable exception, + String accountId, + CacheHttpRequest request, + long startTime) { + + logger.warn("Error occurred while interacting with cache service: {}", exception.getMessage()); + logger.debug("Error occurred while interacting with cache service", exception); + + metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); + + final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), request, null, startTime); + return CacheServiceResult.of(httpCall, exception, Collections.emptyMap()); + } + + private DebugHttpCall makeDebugHttpCall(String endpoint, + CacheHttpRequest httpRequest, + CacheHttpResponse httpResponse, + long startTime) { + + return DebugHttpCall.builder() + .endpoint(endpoint) + .requestUri(httpRequest != null ? httpRequest.getUri() : null) + .requestBody(httpRequest != null ? httpRequest.getBody() : null) + .responseStatus(httpResponse != null ? httpResponse.getStatusCode() : null) + .responseBody(httpResponse != null ? httpResponse.getBody() : null) + .responseTimeMillis(responseTime(startTime)) + .requestHeaders(debugHeaders) + .build(); + } + + private int responseTime(long startTime) { + return Math.toIntExact(clock.millis() - startTime); + } + + private CachedCreative createJsonPutObjectOpenrtb(CacheBid cacheBid, + String accountId, + EventsContext eventsContext) { + + final BidInfo bidInfo = cacheBid.getBidInfo(); + final Bid bid = bidInfo.getBid(); + final ObjectNode bidObjectNode = mapper.mapper().valueToTree(bid); + + final String eventUrl = generateWinUrl( + bidInfo.getBidId(), + bidInfo.getBidder(), + accountId, + eventsContext); + if (eventUrl != null) { + bidObjectNode.put(BID_WURL_ATTRIBUTE, eventUrl); + } + + final BidPutObject payload = BidPutObject.builder() + .aid(eventsContext.getAuctionId()) + .type("json") + .value(bidObjectNode) + .ttlseconds(cacheBid.getTtl()) + .build(); + + return CachedCreative.of(payload, creativeSizeFromAdm(bid.getAdm())); + } + + private CachedCreative createXmlPutObjectOpenrtb(CacheBid cacheBid, String requestId, String hbCacheId) { + final BidInfo bidInfo = cacheBid.getBidInfo(); + final Bid bid = bidInfo.getBid(); + final String vastXml = bid.getAdm(); + + final String customCacheKey = resolveCustomCacheKey(hbCacheId, bidInfo.getCategory()); + + final BidPutObject payload = BidPutObject.builder() + .aid(requestId) + .type("xml") + .value(vastXml != null ? new TextNode(vastXml) : null) + .ttlseconds(cacheBid.getTtl()) + .key(customCacheKey) + .build(); + + return CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue())); + } + + private static String resolveCustomCacheKey(String hbCacheId, String category) { + return StringUtils.isNoneEmpty(category, hbCacheId) + ? "%s_%s".formatted(category, hbCacheId) + : null; + } + + private String generateWinUrl(String bidId, + String bidder, + String accountId, + EventsContext eventsContext) { + + return eventsContext.isEnabledForAccount() && eventsContext.isEnabledForRequest() + ? eventsService.winUrl( + bidId, + bidder, + accountId, + true, + eventsContext) + : null; + } + + private BidCacheResponse toBidCacheResponse(int statusCode, + String responseBody, + int bidCount, + String accountId, + long startTime) { + + if (statusCode != 200) { + throw new PreBidException("HTTP status code " + statusCode); + } + + final BidCacheResponse bidCacheResponse; + try { + bidCacheResponse = mapper.decodeValue(responseBody, BidCacheResponse.class); + } catch (DecodeException e) { + throw new PreBidException("Cannot parse response: " + responseBody, e); + } + + final List responses = bidCacheResponse.getResponses(); + if (responses == null || responses.size() != bidCount) { + throw new PreBidException("The number of response cache objects doesn't match with bids"); + } + + metrics.updateCacheRequestSuccessTime(accountId, clock.millis() - startTime); + return bidCacheResponse; + } + + private List toResponse(BidCacheResponse bidCacheResponse, Function responseItemCreator) { + return bidCacheResponse.getResponses().stream() + .filter(Objects::nonNull) + .map(responseItemCreator) + .filter(Objects::nonNull) + .toList(); + } + + private static Map toResultMap(List cacheBids, + List cacheVideoBids, + List uuids, + String hbCacheId) { + + final Map result = new HashMap<>(uuids.size()); + + // here we assume "videoBids" is a sublist of "bids" + // so, no need for a separate loop on "videoBids" if "bids" is not empty + if (!cacheBids.isEmpty()) { + final List videoBids = cacheVideoBids.stream() + .map(CacheBid::getBidInfo) + .map(BidInfo::getBid) + .toList(); + + final int bidsSize = cacheBids.size(); + for (int i = 0; i < bidsSize; i++) { + final CacheBid cacheBid = cacheBids.get(i); + final BidInfo bidInfo = cacheBid.getBidInfo(); + final Bid bid = bidInfo.getBid(); + final Integer ttl = cacheBid.getTtl(); + + // determine uuid for video bid + final int indexOfVideoBid = videoBids.indexOf(bid); + final String videoBidUuid = indexOfVideoBid != -1 ? uuids.get(bidsSize + indexOfVideoBid) : null; + final Integer videoTtl = indexOfVideoBid != -1 ? cacheVideoBids.get(indexOfVideoBid).getTtl() : null; + + result.put(bid, CacheInfo.of(uuids.get(i), resolveVideoBidUuid(videoBidUuid, hbCacheId), ttl, + videoTtl)); + } + } else { + for (int i = 0; i < cacheVideoBids.size(); i++) { + final CacheBid cacheBid = cacheVideoBids.get(i); + final BidInfo bidInfo = cacheBid.getBidInfo(); + result.put(bidInfo.getBid(), CacheInfo.of(null, resolveVideoBidUuid(uuids.get(i), hbCacheId), null, + cacheBid.getTtl())); + } + } + + return result; + } + + private static String resolveVideoBidUuid(String uuid, String hbCacheId) { + return hbCacheId != null && uuid.endsWith(hbCacheId) ? hbCacheId : uuid; + } + + private void updateCreativeMetrics(String accountId, List cachedCreatives) { + for (final CachedCreative cachedCreative : cachedCreatives) { + metrics.updateCacheCreativeSize(accountId, + cachedCreative.getSize(), + resolveCreativeTypeName(cachedCreative.getPayload())); + } + } + + private static MetricName resolveCreativeTypeName(BidPutObject bidPutObject) { + final String typeValue = ObjectUtil.getIfNotNull(bidPutObject, BidPutObject::getType); + + if (Objects.equals(typeValue, CacheServiceUtil.XML_CREATIVE_TYPE)) { + return MetricName.xml; + } + + if (Objects.equals(typeValue, CacheServiceUtil.JSON_CREATIVE_TYPE)) { + return MetricName.json; + } + + return MetricName.unknown; + } + + private static int creativeSizeFromAdm(String adm) { + return lengthOrZero(adm); + } + + private static int lengthOrZero(String adm) { + return adm != null ? adm.length() : 0; + } + + private static int creativeSizeFromTextNode(JsonNode node) { + return node != null ? node.asText().length() : 0; + } + + private BidCacheRequest toBidCacheRequest(List cachedCreatives) { + return BidCacheRequest.of(cachedCreatives.stream() + .map(CachedCreative::getPayload) + .toList()); + } +} diff --git a/src/main/java/org/prebid/server/cache/PbcStorageService.java b/src/main/java/org/prebid/server/cache/PbcStorageService.java new file mode 100644 index 00000000000..c76f0884d52 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/PbcStorageService.java @@ -0,0 +1,40 @@ +package org.prebid.server.cache; + +import io.vertx.core.Future; +import org.prebid.server.cache.proto.request.module.StorageDataType; +import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; + +public interface PbcStorageService { + + Future storeEntry(String key, + String value, + StorageDataType type, + Integer ttlseconds, + String application, + String appCode); + + Future retrieveEntry(String key, String appCode, String application); + + static NoOpPbcStorageService noOp() { + return new NoOpPbcStorageService(); + } + + class NoOpPbcStorageService implements PbcStorageService { + + @Override + public Future storeEntry(String key, + String value, + StorageDataType type, + Integer ttlseconds, + String application, + String appCode) { + + return Future.succeededFuture(); + } + + @Override + public Future retrieveEntry(String key, String appCode, String application) { + return Future.succeededFuture(ModuleCacheResponse.empty()); + } + } +} diff --git a/src/main/java/org/prebid/server/cache/model/CacheBid.java b/src/main/java/org/prebid/server/cache/model/CacheBid.java index a10ab9a7a3b..0cac7158597 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheBid.java +++ b/src/main/java/org/prebid/server/cache/model/CacheBid.java @@ -4,10 +4,10 @@ import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.auction.model.BidInfo; -import org.prebid.server.cache.CacheService; +import org.prebid.server.cache.CoreCacheService; /** - * Holds the information about cache TTL for particular {@link Bid} to be send to {@link CacheService}. + * Holds the information about cache TTL for particular {@link Bid} to be send to {@link CoreCacheService}. */ @AllArgsConstructor(staticName = "of") @Value diff --git a/src/main/java/org/prebid/server/cache/model/CacheContext.java b/src/main/java/org/prebid/server/cache/model/CacheContext.java index 4d64bf6e3ad..377735c125f 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheContext.java +++ b/src/main/java/org/prebid/server/cache/model/CacheContext.java @@ -12,9 +12,5 @@ public class CacheContext { boolean shouldCacheBids; - Integer cacheBidsTtl; - boolean shouldCacheVideoBids; - - Integer cacheVideoBidsTtl; } diff --git a/src/main/java/org/prebid/server/cache/model/CachedCreative.java b/src/main/java/org/prebid/server/cache/model/CachedCreative.java new file mode 100644 index 00000000000..50040d67e36 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/model/CachedCreative.java @@ -0,0 +1,12 @@ +package org.prebid.server.cache.model; + +import lombok.Value; +import org.prebid.server.cache.proto.request.bid.BidPutObject; + +@Value(staticConstructor = "of") +public class CachedCreative { + + BidPutObject payload; + + int size; +} diff --git a/src/main/java/org/prebid/server/cache/proto/request/BidCacheRequest.java b/src/main/java/org/prebid/server/cache/proto/request/BidCacheRequest.java deleted file mode 100644 index f8c1cd37496..00000000000 --- a/src/main/java/org/prebid/server/cache/proto/request/BidCacheRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.cache.proto.request; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class BidCacheRequest { - - List puts; -} diff --git a/src/main/java/org/prebid/server/cache/proto/request/PutObject.java b/src/main/java/org/prebid/server/cache/proto/request/PutObject.java deleted file mode 100644 index 8c2b04e62a3..00000000000 --- a/src/main/java/org/prebid/server/cache/proto/request/PutObject.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.prebid.server.cache.proto.request; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Builder; -import lombok.Value; - -@Builder(toBuilder = true) -@Value -public class PutObject { - - String type; - - JsonNode value; - - Integer expiry; - - Integer ttlseconds; - - String aid; - - String key; - - String bidid; // this is "/vtrack" specific - - String bidder; // this is "/vtrack" specific - - Long timestamp; // this is "/vtrack" specific -} diff --git a/src/main/java/org/prebid/server/cache/proto/request/bid/BidCacheRequest.java b/src/main/java/org/prebid/server/cache/proto/request/bid/BidCacheRequest.java new file mode 100644 index 00000000000..7509bad3006 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/request/bid/BidCacheRequest.java @@ -0,0 +1,11 @@ +package org.prebid.server.cache.proto.request.bid; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class BidCacheRequest { + + List puts; +} diff --git a/src/main/java/org/prebid/server/cache/proto/request/bid/BidPutObject.java b/src/main/java/org/prebid/server/cache/proto/request/bid/BidPutObject.java new file mode 100644 index 00000000000..d34294537bf --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/request/bid/BidPutObject.java @@ -0,0 +1,28 @@ +package org.prebid.server.cache.proto.request.bid; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class BidPutObject { + + String type; + + JsonNode value; + + Integer expiry; + + Integer ttlseconds; + + String aid; + + String key; + + String bidid; // this is "/vtrack" specific + + String bidder; // this is "/vtrack" specific + + Long timestamp; // this is "/vtrack" specific +} diff --git a/src/main/java/org/prebid/server/cache/proto/request/module/ModuleCacheRequest.java b/src/main/java/org/prebid/server/cache/proto/request/module/ModuleCacheRequest.java new file mode 100644 index 00000000000..25c7a08309a --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/request/module/ModuleCacheRequest.java @@ -0,0 +1,17 @@ +package org.prebid.server.cache.proto.request.module; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ModuleCacheRequest { + + String key; + + StorageDataType type; + + String value; + + String application; + + Integer ttlseconds; +} diff --git a/src/main/java/org/prebid/server/cache/proto/request/module/StorageDataType.java b/src/main/java/org/prebid/server/cache/proto/request/module/StorageDataType.java new file mode 100644 index 00000000000..7636d1c75ea --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/request/module/StorageDataType.java @@ -0,0 +1,17 @@ +package org.prebid.server.cache.proto.request.module; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum StorageDataType { + + JSON("json"), + XML("xml"), + TEXT("text"); + + @JsonValue + private final String text; + + StorageDataType(String text) { + this.text = text; + } +} diff --git a/src/main/java/org/prebid/server/cache/proto/response/BidCacheResponse.java b/src/main/java/org/prebid/server/cache/proto/response/BidCacheResponse.java deleted file mode 100644 index 8e207bcbf6a..00000000000 --- a/src/main/java/org/prebid/server/cache/proto/response/BidCacheResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.cache.proto.response; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class BidCacheResponse { - - List responses; -} diff --git a/src/main/java/org/prebid/server/cache/proto/response/CacheObject.java b/src/main/java/org/prebid/server/cache/proto/response/CacheObject.java deleted file mode 100644 index 448e9a51c9c..00000000000 --- a/src/main/java/org/prebid/server/cache/proto/response/CacheObject.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.cache.proto.response; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class CacheObject { - - String uuid; -} diff --git a/src/main/java/org/prebid/server/cache/proto/response/bid/BidCacheResponse.java b/src/main/java/org/prebid/server/cache/proto/response/bid/BidCacheResponse.java new file mode 100644 index 00000000000..20133906f8e --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/response/bid/BidCacheResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.cache.proto.response.bid; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class BidCacheResponse { + + List responses; +} diff --git a/src/main/java/org/prebid/server/cache/proto/response/bid/CacheObject.java b/src/main/java/org/prebid/server/cache/proto/response/bid/CacheObject.java new file mode 100644 index 00000000000..179703274c2 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/response/bid/CacheObject.java @@ -0,0 +1,9 @@ +package org.prebid.server.cache.proto.response.bid; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class CacheObject { + + String uuid; +} diff --git a/src/main/java/org/prebid/server/cache/proto/response/module/ModuleCacheResponse.java b/src/main/java/org/prebid/server/cache/proto/response/module/ModuleCacheResponse.java new file mode 100644 index 00000000000..494acdab836 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/response/module/ModuleCacheResponse.java @@ -0,0 +1,18 @@ +package org.prebid.server.cache.proto.response.module; + +import lombok.Value; +import org.prebid.server.cache.proto.request.module.StorageDataType; + +@Value(staticConstructor = "of") +public class ModuleCacheResponse { + + String key; + + StorageDataType type; + + String value; + + public static ModuleCacheResponse empty() { + return ModuleCacheResponse.of(null, null, null); + } +} diff --git a/src/main/java/org/prebid/server/cache/utils/CacheServiceUtil.java b/src/main/java/org/prebid/server/cache/utils/CacheServiceUtil.java new file mode 100644 index 00000000000..fdde5f610c0 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/utils/CacheServiceUtil.java @@ -0,0 +1,44 @@ +package org.prebid.server.cache.utils; + +import io.vertx.core.MultiMap; +import org.prebid.server.util.HttpUtil; + +import java.net.MalformedURLException; +import java.net.URL; + +public class CacheServiceUtil { + + public static final MultiMap CACHE_HEADERS = HttpUtil.headers(); + public static final String XML_CREATIVE_TYPE = "xml"; + public static final String JSON_CREATIVE_TYPE = "json"; + + private CacheServiceUtil() { + } + + public static URL getCacheEndpointUrl(String cacheSchema, String cacheHost, String path) { + try { + final URL baseUrl = getCacheBaseUrl(cacheSchema, cacheHost); + return new URL(baseUrl, path); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Could not get cache endpoint for prebid cache service", e); + } + } + + private static URL getCacheBaseUrl(String cacheSchema, String cacheHost) throws MalformedURLException { + return new URL(cacheSchema + "://" + cacheHost); + } + + public static String getCachedAssetUrlTemplate(String cacheSchema, + String cacheHost, + String path, + String cacheQuery) { + + try { + final URL baseUrl = getCacheBaseUrl(cacheSchema, cacheHost); + return new URL(baseUrl, path + "?" + cacheQuery).toString(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Could not get cached asset url template for prebid cache service", e); + } + } + +} diff --git a/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java b/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java index 0adeb6d8904..9b18c8af24d 100644 --- a/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java +++ b/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java @@ -17,7 +17,6 @@ import org.prebid.server.settings.model.AccountPrivacySandboxCookieDeprecationConfig; import org.prebid.server.util.HttpUtil; -import java.util.Objects; import java.util.Optional; public class CookieDeprecationService { @@ -26,20 +25,12 @@ public class CookieDeprecationService { private static final String DEVICE_EXT_COOKIE_DEPRECATION_FIELD_NAME = "cdep"; private static final long DEFAULT_MAX_AGE = 604800L; - private final Account defaultAccount; - - public CookieDeprecationService(Account defaultAccount) { - this.defaultAccount = Objects.requireNonNull(defaultAccount); - } - public PartitionedCookie makeCookie(Account account, RoutingContext routingContext) { - final Account resolvedAccount = account.isEmpty() ? defaultAccount : account; - - if (hasDeprecationCookieInRequest(routingContext) || isCookieDeprecationDisabled(resolvedAccount)) { + if (hasDeprecationCookieInRequest(routingContext) || isCookieDeprecationDisabled(account)) { return null; } - final Long maxAge = getCookieDeprecationConfig(resolvedAccount) + final Long maxAge = getCookieDeprecationConfig(account) .map(AccountPrivacySandboxCookieDeprecationConfig::getTtlSec) .orElse(DEFAULT_MAX_AGE); @@ -61,13 +52,9 @@ public BidRequest updateBidRequestDevice(BidRequest bidRequest, AuctionContext a .get(HttpUtil.SEC_COOKIE_DEPRECATION); final Account account = auctionContext.getAccount(); - final Account resolvedAccount = account.isEmpty() ? defaultAccount : account; final Device device = bidRequest.getDevice(); - if (secCookieDeprecation == null - || containsCookieDeprecation(device) - || isCookieDeprecationDisabled(resolvedAccount)) { - + if (secCookieDeprecation == null || containsCookieDeprecation(device) || isCookieDeprecationDisabled(account)) { return bidRequest; } diff --git a/src/main/java/org/prebid/server/cookie/CookieSyncService.java b/src/main/java/org/prebid/server/cookie/CookieSyncService.java index 074fab249af..2d381bfa665 100644 --- a/src/main/java/org/prebid/server/cookie/CookieSyncService.java +++ b/src/main/java/org/prebid/server/cookie/CookieSyncService.java @@ -497,7 +497,7 @@ private List aliasSyncedAsRootStatuses(Set bidders final Set allowedRequestedBidders = cookieSyncContext.getBiddersContext().allowedRequestedBidders(); return biddersToSync.stream() - .filter(bidder -> allowedRequestedBidders.contains(bidder)) + .filter(allowedRequestedBidders::contains) .filter(this::isAliasSyncedAsRootFamily) .map(this::warningForAliasSyncedAsRootFamily) .toList(); diff --git a/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java b/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java index 9a2cd932a09..d3c0dae7150 100644 --- a/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java +++ b/src/main/java/org/prebid/server/cookie/PrioritizedCoopSyncProvider.java @@ -1,8 +1,8 @@ package org.prebid.server.cookie; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountCookieSyncConfig; @@ -39,15 +39,15 @@ private static Set validCoopSyncBidders(Set bidders, BidderCatal for (String bidder : bidders) { if (!bidderCatalog.isValidName(bidder)) { logger.info(""" - bidder {0} is provided for prioritized coop-syncing, \ + bidder {} is provided for prioritized coop-syncing, \ but is invalid bidder name, ignoring""", bidder); } else if (!bidderCatalog.isActive(bidder)) { logger.info(""" - bidder {0} is provided for prioritized coop-syncing, \ + bidder {} is provided for prioritized coop-syncing, \ but disabled in current pbs instance, ignoring""", bidder); } else if (bidderCatalog.usersyncerByName(bidder).isEmpty()) { logger.info(""" - bidder {0} is provided for prioritized coop-syncing, \ + bidder {} is provided for prioritized coop-syncing, \ but has no user-sync configuration, ignoring""", bidder); } else { validBidders.add(bidder); diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 7416abfdfa2..6b608f06307 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -3,8 +3,6 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.Cookie; import io.vertx.core.http.CookieSameSite; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.StringUtils; import org.prebid.server.cookie.model.UidWithExpiry; @@ -12,6 +10,8 @@ import org.prebid.server.cookie.proto.Uids; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.util.HttpUtil; @@ -131,7 +131,7 @@ public Uids parseUids(Map cookies) { try { return mapper.decodeValue(Buffer.buffer(Base64.getUrlDecoder().decode(cookieValue)), Uids.class); } catch (IllegalArgumentException | DecodeException e) { - logger.debug("Could not decode or parse {0} cookie value {1}", e, COOKIE_NAME, cookieValue); + logger.debug("Could not decode or parse {} cookie value {}", e, COOKIE_NAME, cookieValue); } } return null; diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index cc63eb5e927..5adba78d6e8 100644 --- a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java +++ b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java @@ -2,23 +2,24 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import io.vertx.core.Promise; import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.currency.proto.CurrencyConversionRates; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.Initializable; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.io.IOException; import java.math.BigDecimal; @@ -26,6 +27,7 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; @@ -66,19 +68,23 @@ public CurrencyConversionService(ExternalConversionProperties externalConversion * Must be called on Vertx event loop thread. */ @Override - public void initialize() { + public void initialize(Promise initializePromise) { if (externalConversionProperties != null) { final Long refreshPeriod = externalConversionProperties.getRefreshPeriodMs(); final Long defaultTimeout = externalConversionProperties.getDefaultTimeoutMs(); final HttpClient httpClient = externalConversionProperties.getHttpClient(); final Vertx vertx = externalConversionProperties.getVertx(); - vertx.setPeriodic(refreshPeriod, ignored -> populatesLatestCurrencyRates(currencyServerUrl, defaultTimeout, + vertx.setPeriodic(refreshPeriod, ignored -> populatesLatestCurrencyRates( + currencyServerUrl, + defaultTimeout, httpClient)); populatesLatestCurrencyRates(currencyServerUrl, defaultTimeout, httpClient); externalConversionProperties.getMetrics().createCurrencyRatesGauge(this::isRatesStale); } + + initializePromise.tryComplete(); } /** @@ -272,7 +278,13 @@ private static BigDecimal getConversionRate(Map> return conversionRate; } - return findIntermediateConversionRate(directCurrencyRates, reverseCurrencyRates); + final BigDecimal intermediateConversionRate = findIntermediateConversionRate(directCurrencyRates, + reverseCurrencyRates); + if (intermediateConversionRate != null) { + return intermediateConversionRate; + } + + return findCrossConversionRate(currencyConversionRates, fromCurrency, toCurrency); } /** @@ -286,7 +298,8 @@ private static BigDecimal findReverseConversionRate(Map curr : null; return reverseConversionRate != null - ? BigDecimal.ONE.divide(reverseConversionRate, reverseConversionRate.precision(), + ? BigDecimal.ONE.divide(reverseConversionRate, + getRatePrecision(reverseConversionRate), RoundingMode.HALF_EVEN) : null; } @@ -305,20 +318,43 @@ private static BigDecimal findIntermediateConversionRate(Map if (!sharedCurrencies.isEmpty()) { // pick any found shared currency - final String sharedCurrency = sharedCurrencies.get(0); + final String sharedCurrency = sharedCurrencies.getFirst(); final BigDecimal directCurrencyRateIntermediate = directCurrencyRates.get(sharedCurrency); final BigDecimal reverseCurrencyRateIntermediate = reverseCurrencyRates.get(sharedCurrency); conversionRate = directCurrencyRateIntermediate.divide(reverseCurrencyRateIntermediate, // chose largest precision among intermediate rates - reverseCurrencyRateIntermediate.compareTo(directCurrencyRateIntermediate) > 0 - ? reverseCurrencyRateIntermediate.precision() - : directCurrencyRateIntermediate.precision(), + getRatePrecision(directCurrencyRateIntermediate, reverseCurrencyRateIntermediate), RoundingMode.HALF_EVEN); } } return conversionRate; } + private static BigDecimal findCrossConversionRate(Map> currencyConversionRates, + String fromCurrency, + String toCurrency) { + for (Map rates : currencyConversionRates.values()) { + final BigDecimal fromRate = rates.get(fromCurrency); + final BigDecimal toRate = rates.get(toCurrency); + if (fromRate != null && toRate != null) { + return toRate.divide(fromRate, + getRatePrecision(fromRate, toRate), + RoundingMode.HALF_EVEN); + } + } + + return null; + } + + private static int getRatePrecision(BigDecimal... rates) { + final int precision = Arrays.stream(rates) + .map(BigDecimal::precision) + .max(Integer::compareTo) + .orElse(DEFAULT_PRICE_PRECISION); + + return Math.max(precision, DEFAULT_PRICE_PRECISION); + } + private boolean isRatesStale() { if (lastUpdated == null) { return false; diff --git a/src/main/java/org/prebid/server/deals/AdminCentralService.java b/src/main/java/org/prebid/server/deals/AdminCentralService.java deleted file mode 100644 index f5bd033b051..00000000000 --- a/src/main/java/org/prebid/server/deals/AdminCentralService.java +++ /dev/null @@ -1,239 +0,0 @@ -package org.prebid.server.deals; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.deals.events.AdminEventProcessor; -import org.prebid.server.deals.model.AdminAccounts; -import org.prebid.server.deals.model.AdminCentralResponse; -import org.prebid.server.deals.model.AdminLineItems; -import org.prebid.server.deals.model.Command; -import org.prebid.server.deals.model.LogTracer; -import org.prebid.server.deals.model.ServicesCommand; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.CriteriaManager; -import org.prebid.server.settings.CachingApplicationSettings; -import org.prebid.server.settings.SettingsCache; -import org.prebid.server.settings.proto.request.InvalidateSettingsCacheRequest; -import org.prebid.server.settings.proto.request.UpdateSettingsCacheRequest; -import org.prebid.server.util.ObjectUtil; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class AdminCentralService implements AdminEventProcessor { - - private static final Logger logger = LoggerFactory.getLogger(AdminCentralService.class); - - private static final String START = "start"; - private static final String STOP = "stop"; - private static final String INVALIDATE = "invalidate"; - private static final String SAVE = "save"; - private static final String STORED_REQUEST_CACHE = "stored request cache"; - private static final String AMP_STORED_REQUEST_CACHE = "amp stored request cache"; - - private final CriteriaManager criteriaManager; - private final LineItemService lineItemService; - private final DeliveryProgressService deliveryProgressService; - private final SettingsCache settingsCache; - private final SettingsCache ampSettingsCache; - private final CachingApplicationSettings cachingApplicationSettings; - private final JacksonMapper mapper; - private final List suspendableServices; - - public AdminCentralService(CriteriaManager criteriaManager, - LineItemService lineItemService, - DeliveryProgressService deliveryProgressService, - SettingsCache settingsCache, - SettingsCache ampSettingsCache, - CachingApplicationSettings cachingApplicationSettings, - JacksonMapper mapper, - List suspendableServices) { - this.criteriaManager = Objects.requireNonNull(criteriaManager); - this.lineItemService = Objects.requireNonNull(lineItemService); - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.settingsCache = settingsCache; - this.ampSettingsCache = ampSettingsCache; - this.cachingApplicationSettings = cachingApplicationSettings; - this.mapper = Objects.requireNonNull(mapper); - this.suspendableServices = Objects.requireNonNull(suspendableServices); - } - - @Override - public void processAdminCentralEvent(AdminCentralResponse centralAdminResponse) { - final LogTracer logTracer = centralAdminResponse.getTracer(); - if (logTracer != null) { - handleLogTracer(centralAdminResponse.getTracer()); - } - - final Command lineItemsCommand = centralAdminResponse.getLineItems(); - if (lineItemsCommand != null) { - handleLineItems(lineItemsCommand); - } - - final Command storedRequestCommand = centralAdminResponse.getStoredRequest(); - if (storedRequestCommand != null && settingsCache != null) { - handleStoredRequest(settingsCache, storedRequestCommand, STORED_REQUEST_CACHE); - } - - final Command storedRequestAmpCommand = centralAdminResponse.getStoredRequestAmp(); - if (storedRequestAmpCommand != null && ampSettingsCache != null) { - handleStoredRequest(ampSettingsCache, storedRequestAmpCommand, AMP_STORED_REQUEST_CACHE); - } - - final Command accountCommand = centralAdminResponse.getAccount(); - if (accountCommand != null && cachingApplicationSettings != null) { - handleAccountCommand(accountCommand); - } - - final ServicesCommand servicesCommand = centralAdminResponse.getServices(); - if (servicesCommand != null) { - handleServiceCommand(servicesCommand); - } - } - - private void handleAccountCommand(Command accountCommand) { - final String cmd = accountCommand.getCmd(); - if (StringUtils.isBlank(cmd)) { - logger.warn("Command for account action was not defined in register response"); - return; - } - - if (!Objects.equals(cmd, INVALIDATE)) { - logger.warn("Account commands supports only `invalidate` command, but received {0}", cmd); - return; - } - - final ObjectNode body = accountCommand.getBody(); - final AdminAccounts adminAccounts; - try { - adminAccounts = body != null - ? mapper.mapper().convertValue(body, AdminAccounts.class) - : null; - } catch (IllegalArgumentException e) { - logger.warn("Can't parse admin accounts body, failed with exception message : {0}", e.getMessage()); - return; - } - - final List accounts = ObjectUtil.getIfNotNull(adminAccounts, AdminAccounts::getAccounts); - if (CollectionUtils.isNotEmpty(accounts)) { - accounts.forEach(cachingApplicationSettings::invalidateAccountCache); - } else { - cachingApplicationSettings.invalidateAllAccountCache(); - } - } - - private void handleLineItems(Command lineItemsCommand) { - final String cmd = lineItemsCommand.getCmd(); - if (StringUtils.isBlank(cmd)) { - logger.warn("Command for line-items action was not defined in register response."); - return; - } - - if (!Objects.equals(cmd, INVALIDATE)) { - logger.warn("Line Items section supports only `invalidate` command, but received {0}", cmd); - return; - } - - final ObjectNode body = lineItemsCommand.getBody(); - final AdminLineItems adminLineItems; - try { - adminLineItems = body != null - ? mapper.mapper().convertValue(body, AdminLineItems.class) - : null; - } catch (IllegalArgumentException e) { - logger.warn("Can't parse admin line items body, failed with exception message : {0}", e.getMessage()); - return; - } - - final List lineItemIds = ObjectUtil.getIfNotNull(adminLineItems, AdminLineItems::getIds); - - if (CollectionUtils.isNotEmpty(lineItemIds)) { - lineItemService.invalidateLineItemsByIds(lineItemIds); - deliveryProgressService.invalidateLineItemsByIds(lineItemIds); - } else { - lineItemService.invalidateLineItems(); - deliveryProgressService.invalidateLineItems(); - } - } - - private void handleStoredRequest(SettingsCache settingsCache, Command storedRequestCommand, String serviceName) { - final String cmd = storedRequestCommand.getCmd(); - if (StringUtils.isBlank(cmd)) { - logger.warn("Command for {0} was not defined.", serviceName); - return; - } - - final ObjectNode body = storedRequestCommand.getBody(); - if (body == null) { - logger.warn("Command body for {0} was not defined.", serviceName); - return; - } - - switch (cmd) { - case INVALIDATE -> invalidateStoredRequests(settingsCache, serviceName, body); - case SAVE -> saveStoredRequests(settingsCache, serviceName, body); - default -> logger.warn("Command for {0} should has value 'save' or 'invalidate' but was {1}.", - serviceName, cmd); - } - } - - private void saveStoredRequests(SettingsCache settingsCache, String serviceName, ObjectNode body) { - final UpdateSettingsCacheRequest saveRequest; - try { - saveRequest = mapper.mapper().convertValue(body, UpdateSettingsCacheRequest.class); - } catch (IllegalArgumentException e) { - logger.warn("Can't parse save settings cache request object for {0}," - + " failed with exception message : {1}", serviceName, e.getMessage()); - return; - } - final Map storedRequests = MapUtils.emptyIfNull(saveRequest.getRequests()); - final Map storedImps = MapUtils.emptyIfNull(saveRequest.getImps()); - settingsCache.save(storedRequests, storedImps); - logger.info("Stored request with ids {0} and stored impressions with ids {1} were successfully saved", - String.join(", ", storedRequests.keySet()), String.join(", ", storedImps.keySet())); - } - - private void invalidateStoredRequests(SettingsCache settingsCache, String serviceName, ObjectNode body) { - final InvalidateSettingsCacheRequest invalidateRequest; - try { - invalidateRequest = mapper.mapper().convertValue(body, InvalidateSettingsCacheRequest.class); - } catch (IllegalArgumentException e) { - logger.warn("Can't parse invalidate settings cache request object for {0}," - + " failed with exception message : {1}", serviceName, e.getMessage()); - return; - } - final List requestIds = ListUtils.emptyIfNull(invalidateRequest.getRequests()); - final List impIds = ListUtils.emptyIfNull(invalidateRequest.getImps()); - settingsCache.invalidate(requestIds, impIds); - logger.info("Stored requests with ids {0} and impression with ids {1} were successfully invalidated", - String.join(", ", requestIds), String.join(", ", impIds)); - } - - private void handleLogTracer(LogTracer logTracer) { - final String command = logTracer.getCmd(); - if (StringUtils.isBlank(command)) { - logger.warn("Command for traceLogger was not defined"); - return; - } - - switch (command) { - case START -> criteriaManager.addCriteria(logTracer.getFilters(), logTracer.getDurationInSeconds()); - case STOP -> criteriaManager.stop(); - default -> logger.warn("Command for trace logger should has value 'start' or 'stop' but was {0}.", command); - } - } - - private void handleServiceCommand(ServicesCommand servicesCommand) { - final String command = servicesCommand.getCmd(); - if (command != null && command.equalsIgnoreCase(STOP)) { - suspendableServices.forEach(Suspendable::suspend); - } - logger.info("PBS services were successfully suspended"); - } -} diff --git a/src/main/java/org/prebid/server/deals/AlertHttpService.java b/src/main/java/org/prebid/server/deals/AlertHttpService.java deleted file mode 100644 index 3b89b0f2bd8..00000000000 --- a/src/main/java/org/prebid/server/deals/AlertHttpService.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.prebid.server.deals; - -import io.netty.handler.codec.http.HttpHeaderValues; -import io.vertx.core.AsyncResult; -import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.deals.model.AlertEvent; -import org.prebid.server.deals.model.AlertPriority; -import org.prebid.server.deals.model.AlertProxyProperties; -import org.prebid.server.deals.model.AlertSource; -import org.prebid.server.deals.model.DeploymentProperties; -import org.prebid.server.json.EncodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class AlertHttpService { - - private static final Logger logger = LoggerFactory.getLogger(AlertHttpService.class); - private static final String RAISE = "RAISE"; - private static final Long DEFAULT_HIGH_ALERT_PERIOD = 15L; - - private final JacksonMapper mapper; - private final HttpClient httpClient; - private final Clock clock; - private final AlertProxyProperties alertProxyProperties; - private final AlertSource alertSource; - private final boolean enabled; - private final String url; - private final long timeoutMillis; - private final String authHeaderValue; - private final Map alertTypes; - private final Map alertTypesCounters; - - public AlertHttpService(JacksonMapper mapper, HttpClient httpClient, Clock clock, - DeploymentProperties deploymentProperties, - AlertProxyProperties alertProxyProperties) { - this.mapper = Objects.requireNonNull(mapper); - this.httpClient = Objects.requireNonNull(httpClient); - this.clock = Objects.requireNonNull(clock); - this.alertProxyProperties = Objects.requireNonNull(alertProxyProperties); - this.alertSource = makeSource(Objects.requireNonNull(deploymentProperties)); - this.enabled = alertProxyProperties.isEnabled(); - this.timeoutMillis = TimeUnit.SECONDS.toMillis(alertProxyProperties.getTimeoutSec()); - this.url = HttpUtil.validateUrl(Objects.requireNonNull(alertProxyProperties.getUrl())); - this.authHeaderValue = HttpUtil.makeBasicAuthHeaderValue(alertProxyProperties.getUsername(), - alertProxyProperties.getPassword()); - this.alertTypes = new ConcurrentHashMap<>(alertProxyProperties.getAlertTypes()); - this.alertTypesCounters = new ConcurrentHashMap<>(alertTypes.keySet().stream() - .collect(Collectors.toMap(Function.identity(), s -> 0L))); - } - - private static AlertSource makeSource(DeploymentProperties deploymentProperties) { - return AlertSource.builder() - .env(deploymentProperties.getProfile()) - .region(deploymentProperties.getPbsRegion()) - .dataCenter(deploymentProperties.getDataCenter()) - .subSystem(deploymentProperties.getSubSystem()) - .system(deploymentProperties.getSystem()) - .hostId(deploymentProperties.getPbsHostId()) - .build(); - } - - public void alertWithPeriod(String serviceName, String alertType, AlertPriority alertPriority, String message) { - if (alertTypes.get(alertType) == null) { - alertTypes.put(alertType, DEFAULT_HIGH_ALERT_PERIOD); - alertTypesCounters.put(alertType, 0L); - } - - long count = alertTypesCounters.get(alertType); - final long period = alertTypes.get(alertType); - - alertTypesCounters.put(alertType, ++count); - final String formattedMessage = "Service %s failed to send request %s time(s) with error message : %s" - .formatted(serviceName, count, message); - if (count == 1) { - alert(alertType, alertPriority, formattedMessage); - } else if (count % period == 0) { - alert(alertType, AlertPriority.HIGH, formattedMessage); - } - } - - public void resetAlertCount(String alertType) { - alertTypesCounters.put(alertType, 0L); - } - - public void alert(String name, AlertPriority alertPriority, String message) { - if (!enabled) { - logger.warn("Alert to proxy is not enabled in pbs configuration"); - return; - } - - final AlertEvent alertEvent = makeEvent(RAISE, alertPriority, name, message, alertSource); - - try { - httpClient.post(alertProxyProperties.getUrl(), headers(), - mapper.encodeToString(Collections.singletonList(alertEvent)), timeoutMillis) - .onComplete(this::handleResponse); - } catch (EncodeException e) { - logger.warn("Can't parse alert proxy payload: {0}", e.getMessage()); - } - } - - private AlertEvent makeEvent(String action, AlertPriority priority, String name, String details, - AlertSource alertSource) { - return AlertEvent.builder() - .id(UUID.randomUUID().toString()) - .action(action.toUpperCase()) - .priority(priority) - .name(name) - .details(details) - .updatedAt(ZonedDateTime.now(clock)) - .source(alertSource) - .build(); - } - - private MultiMap headers() { - return MultiMap.caseInsensitiveMultiMap() - .add(HttpUtil.PG_TRX_ID, UUID.randomUUID().toString()) - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .add(HttpUtil.AUTHORIZATION_HEADER, authHeaderValue); - } - - private void handleResponse(AsyncResult httpClientResponseResult) { - if (httpClientResponseResult.failed()) { - logger.error("Error occurred during sending alert to proxy at {0}::{1} ", url, - httpClientResponseResult.cause().getMessage()); - } - } -} diff --git a/src/main/java/org/prebid/server/deals/DealsService.java b/src/main/java/org/prebid/server/deals/DealsService.java deleted file mode 100644 index 662b6b8dc14..00000000000 --- a/src/main/java/org/prebid/server/deals/DealsService.java +++ /dev/null @@ -1,318 +0,0 @@ -package org.prebid.server.deals; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Deal; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Pmp; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.model.AuctionParticipation; -import org.prebid.server.auction.model.BidderRequest; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.deals.model.MatchLineItemsResult; -import org.prebid.server.deals.proto.LineItemSize; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.CriteriaLogManager; -import org.prebid.server.proto.openrtb.ext.request.ExtDeal; -import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; -import org.prebid.server.util.ObjectUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -public class DealsService { - - private static final Logger logger = LoggerFactory.getLogger(DealsService.class); - - private static final String LINE_FIELD = "line"; - private static final String LINE_BIDDER_FIELD = "bidder"; - private static final String BIDDER_FIELD = "bidder"; - private static final String PG_DEALS_ONLY = "pgdealsonly"; - - private final LineItemService lineItemService; - private final JacksonMapper mapper; - private final CriteriaLogManager criteriaLogManager; - - public DealsService(LineItemService lineItemService, - JacksonMapper mapper, - CriteriaLogManager criteriaLogManager) { - - this.lineItemService = Objects.requireNonNull(lineItemService); - this.mapper = Objects.requireNonNull(mapper); - this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); - } - - public BidderRequest matchAndPopulateDeals(BidderRequest bidderRequest, - BidderAliases aliases, - AuctionContext context) { - - final String bidder = bidderRequest.getBidder(); - final BidRequest bidRequest = bidderRequest.getBidRequest(); - - final Map> impIdToDeals = match(bidRequest, bidder, aliases, context); - final BidRequest modifiedRequest = populateDeals(bidRequest, impIdToDeals, combinerFor(bidder, aliases)); - - return bidderRequest.toBuilder() - .impIdToDeals(impIdToDeals) - .bidRequest(modifiedRequest) - .build(); - } - - private Map> match(BidRequest bidRequest, - String bidder, - BidderAliases aliases, - AuctionContext context) { - - final boolean accountHasDeals = lineItemService.accountHasDeals(context); - if (!accountHasDeals) { - return Collections.emptyMap(); - } - - final Map> impIdToDeals = new HashMap<>(); - for (Imp imp : bidRequest.getImp()) { - final MatchLineItemsResult matchResult = lineItemService.findMatchingLineItems( - bidRequest, imp, bidder, aliases, context); - final List lineItems = matchResult.getLineItems(); - - final List deals = lineItems.stream() - .peek(this::logLineItem) - .map(lineItem -> toDeal(lineItem, imp)) - .toList(); - - if (!deals.isEmpty()) { - impIdToDeals.put(imp.getId(), deals); - } - } - - return impIdToDeals; - } - - private void logLineItem(LineItem lineItem) { - criteriaLogManager.log( - logger, - lineItem.getAccountId(), - lineItem.getSource(), - lineItem.getLineItemId(), - "LineItem %s is ready to be served".formatted(lineItem.getLineItemId()), logger::debug); - } - - private Deal toDeal(LineItem lineItem, Imp imp) { - return Deal.builder() - .id(lineItem.getDealId()) - .ext(mapper.mapper().valueToTree(ExtDeal.of(toExtDealLine(imp, lineItem)))) - .build(); - } - - private static ExtDealLine toExtDealLine(Imp imp, LineItem lineItem) { - final List formats = ObjectUtil.getIfNotNull(imp.getBanner(), Banner::getFormat); - final List lineItemSizes = lineItem.getSizes(); - - final List lineSizes = CollectionUtils.isNotEmpty(formats) && CollectionUtils.isNotEmpty(lineItemSizes) - ? intersectionOf(formats, lineItemSizes) - : null; - - return ExtDealLine.of(lineItem.getLineItemId(), lineItem.getExtLineItemId(), lineSizes, lineItem.getSource()); - } - - private static List intersectionOf(List formats, List lineItemSizes) { - final Set formatsSet = new HashSet<>(formats); - final Set lineItemFormatsSet = lineItemSizes.stream() - .map(size -> Format.builder().w(size.getW()).h(size.getH()).build()) - .collect(Collectors.toSet()); - - final List matchedSizes = lineItemFormatsSet.stream() - .filter(formatsSet::contains) - .toList(); - - return CollectionUtils.isNotEmpty(matchedSizes) ? matchedSizes : null; - } - - private static BiFunction, List, List> combinerFor(String bidder, BidderAliases aliases) { - return (originalDeals, matchedDeals) -> - Stream.concat( - originalDeals.stream().filter(deal -> isDealCorrespondsToBidder(deal, bidder, aliases)), - matchedDeals.stream()) - .map(DealsService::prepareDealForExchange) - .toList(); - } - - private static boolean isDealCorrespondsToBidder(Deal deal, String bidder, BidderAliases aliases) { - final JsonNode extLineBidder = extLineBidder(deal); - if (!isTextual(extLineBidder)) { - return true; - } - - return aliases.isSame(extLineBidder.textValue(), bidder); - } - - private static JsonNode extLineBidder(Deal deal) { - final ObjectNode ext = deal != null ? deal.getExt() : null; - final JsonNode extLine = ext != null ? ext.get(LINE_FIELD) : null; - return extLine != null ? extLine.get(LINE_BIDDER_FIELD) : null; - } - - private static boolean isTextual(JsonNode jsonNode) { - return jsonNode != null && jsonNode.isTextual(); - } - - private static Deal prepareDealForExchange(Deal deal) { - final JsonNode extLineBidder = extLineBidder(deal); - if (!isTextual(extLineBidder)) { - return deal; - } - - final ObjectNode updatedExt = deal.getExt().deepCopy(); - - final ObjectNode updatedExtLine = (ObjectNode) updatedExt.get(LINE_FIELD); - updatedExtLine.remove(LINE_BIDDER_FIELD); - - if (updatedExtLine.isEmpty()) { - updatedExt.remove(LINE_FIELD); - } - - return deal.toBuilder().ext(!updatedExt.isEmpty() ? updatedExt : null).build(); - } - - public static BidRequest populateDeals(BidRequest bidRequest, Map> impIdToDeals) { - return populateDeals(bidRequest, impIdToDeals, ListUtils::union); - } - - private static BidRequest populateDeals(BidRequest bidRequest, - Map> impIdToDeals, - BiFunction, List, List> dealsCombiner) { - - final List originalImps = bidRequest.getImp(); - final List updatedImp = originalImps.stream() - .map(imp -> populateDeals(imp, impIdToDeals.get(imp.getId()), dealsCombiner)) - .toList(); - - if (updatedImp.stream().allMatch(Objects::isNull)) { - return bidRequest; - } - - return bidRequest.toBuilder() - .imp(IntStream.range(0, originalImps.size()) - .mapToObj(i -> ObjectUtils.defaultIfNull(updatedImp.get(i), originalImps.get(i))) - .toList()) - .build(); - } - - private static Imp populateDeals(Imp imp, - List matchedDeals, - BiFunction, List, List> dealsCombiner) { - - final Pmp pmp = imp.getPmp(); - final List originalDeal = pmp != null ? pmp.getDeals() : null; - - final List combinedDeals = dealsCombiner.apply( - ListUtils.emptyIfNull(originalDeal), - ListUtils.emptyIfNull(matchedDeals)); - if (CollectionUtils.isEmpty(combinedDeals)) { - return null; - } - - final Pmp.PmpBuilder pmpBuilder = pmp != null ? pmp.toBuilder() : Pmp.builder(); - return imp.toBuilder() - .pmp(pmpBuilder.deals(combinedDeals).build()) - .build(); - } - - public static List removePgDealsOnlyImpsWithoutDeals( - List auctionParticipations, - AuctionContext context) { - - return auctionParticipations.stream() - .map(auctionParticipation -> removePgDealsOnlyImpsWithoutDeals(auctionParticipation, context)) - .filter(Objects::nonNull) - .toList(); - } - - private static AuctionParticipation removePgDealsOnlyImpsWithoutDeals(AuctionParticipation auctionParticipation, - AuctionContext context) { - - final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); - final String bidder = bidderRequest.getBidder(); - final BidRequest bidRequest = bidderRequest.getBidRequest(); - final List imps = bidRequest.getImp(); - - final Set impsIndicesToRemove = IntStream.range(0, imps.size()) - .filter(i -> isPgDealsOnly(imps.get(i))) - .filter(i -> !havePgDeal(imps.get(i), bidderRequest.getImpIdToDeals())) - .boxed() - .collect(Collectors.toSet()); - - if (impsIndicesToRemove.isEmpty()) { - return auctionParticipation; - } - if (impsIndicesToRemove.size() == imps.size()) { - logImpsExclusion(context, bidder, imps); - return null; - } - - final List impsToRemove = new ArrayList<>(); - final List filteredImps = new ArrayList<>(); - for (int i = 0; i < imps.size(); i++) { - final Imp imp = imps.get(i); - if (impsIndicesToRemove.contains(i)) { - impsToRemove.add(imp); - } else { - filteredImps.add(imp); - } - } - - logImpsExclusion(context, bidder, impsToRemove); - - return auctionParticipation.toBuilder() - .bidderRequest(bidderRequest.toBuilder() - .bidRequest(bidRequest.toBuilder() - .imp(filteredImps) - .build()) - .build()) - .build(); - } - - private static boolean isPgDealsOnly(Imp imp) { - final JsonNode extBidder = imp.getExt().get(BIDDER_FIELD); - if (extBidder == null || !extBidder.isObject()) { - return false; - } - - final JsonNode pgDealsOnlyNode = extBidder.path(PG_DEALS_ONLY); - return pgDealsOnlyNode.isBoolean() && pgDealsOnlyNode.asBoolean(); - } - - private static boolean havePgDeal(Imp imp, Map> impIdToDeals) { - return impIdToDeals != null && CollectionUtils.isNotEmpty(impIdToDeals.get(imp.getId())); - } - - private static void logImpsExclusion(AuctionContext context, - String bidder, - List imps) { - - final String impsIds = imps.stream() - .map(Imp::getId) - .collect(Collectors.joining(", ")); - context.getDebugWarnings().add( - "Not calling %s bidder for impressions %s due to %s flag and no available PG line items." - .formatted(bidder, impsIds, PG_DEALS_ONLY)); - } -} diff --git a/src/main/java/org/prebid/server/deals/DeliveryProgressReportFactory.java b/src/main/java/org/prebid/server/deals/DeliveryProgressReportFactory.java deleted file mode 100644 index 85d3089b79a..00000000000 --- a/src/main/java/org/prebid/server/deals/DeliveryProgressReportFactory.java +++ /dev/null @@ -1,313 +0,0 @@ -package org.prebid.server.deals; - -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.deals.lineitem.DeliveryPlan; -import org.prebid.server.deals.lineitem.DeliveryProgress; -import org.prebid.server.deals.lineitem.DeliveryToken; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.deals.model.DeploymentProperties; -import org.prebid.server.deals.proto.report.DeliveryProgressReport; -import org.prebid.server.deals.proto.report.DeliveryProgressReportBatch; -import org.prebid.server.deals.proto.report.DeliverySchedule; -import org.prebid.server.deals.proto.report.LineItemStatus; -import org.prebid.server.deals.proto.report.LostToLineItem; -import org.prebid.server.deals.proto.report.Token; -import org.prebid.server.util.ObjectUtil; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public class DeliveryProgressReportFactory { - - private static final Logger logger = LoggerFactory.getLogger(DeliveryProgressReportFactory.class); - - private static final LostToLineItemComparator LOST_TO_LINE_ITEM_COMPARATOR = new LostToLineItemComparator(); - - private final DeploymentProperties deploymentProperties; - private final int competitorsNumber; - private final LineItemService lineItemService; - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - - public DeliveryProgressReportFactory( - DeploymentProperties deploymentProperties, int competitorsNumber, LineItemService lineItemService) { - this.deploymentProperties = Objects.requireNonNull(deploymentProperties); - this.competitorsNumber = competitorsNumber; - this.lineItemService = Objects.requireNonNull(lineItemService); - } - - public DeliveryProgressReport fromDeliveryProgress( - DeliveryProgress deliveryProgress, - ZonedDateTime now, - boolean isOverall) { - final List lineItemStatuses = - new ArrayList<>(deliveryProgress.getLineItemStatuses().values()); - return DeliveryProgressReport.builder() - .reportId(UUID.randomUUID().toString()) - .reportTimeStamp(now != null ? formatTimeStamp(now) : null) - .dataWindowStartTimeStamp(isOverall ? null : formatTimeStamp(deliveryProgress.getStartTimeStamp())) - .dataWindowEndTimeStamp(isOverall ? null : formatTimeStamp(deliveryProgress.getEndTimeStamp())) - .instanceId(deploymentProperties.getPbsHostId()) - .region(deploymentProperties.getPbsRegion()) - .vendor(deploymentProperties.getPbsVendor()) - .clientAuctions(deliveryProgress.getRequests().sum()) - .lineItemStatus(makeLineItemStatusReports(deliveryProgress, lineItemStatuses, - deliveryProgress.getLineItemStatuses(), isOverall)) - .build(); - } - - public DeliveryProgressReportBatch batchFromDeliveryProgress( - DeliveryProgress deliveryProgress, - Map overallLineItemStatuses, - ZonedDateTime now, - int batchSize, - boolean isOverall) { - final List lineItemStatuses - = new ArrayList<>(deliveryProgress.getLineItemStatuses().values()); - final String reportId = UUID.randomUUID().toString(); - final String reportTimeStamp = now != null ? formatTimeStamp(now) : null; - final String dataWindowStartTimeStamp = isOverall - ? null - : formatTimeStamp(deliveryProgress.getStartTimeStamp()); - final String dataWindowEndTimeStamp = isOverall ? null : formatTimeStamp(deliveryProgress.getEndTimeStamp()); - final long clientAuctions = deliveryProgress.getRequests().sum(); - - final int lineItemsCount = lineItemStatuses.size(); - final int batchesNumber = lineItemsCount / batchSize + (lineItemsCount % batchSize > 0 ? 1 : 0); - final Set reportsBatch = IntStream.range(0, batchesNumber) - .mapToObj(batchNumber -> updateReportWithLineItems(deliveryProgress, lineItemStatuses, - overallLineItemStatuses, lineItemsCount, batchNumber, batchSize, isOverall)) - .map(deliveryProgressReport -> deliveryProgressReport - .reportId(reportId) - .reportTimeStamp(reportTimeStamp) - .dataWindowStartTimeStamp(dataWindowStartTimeStamp) - .dataWindowEndTimeStamp(dataWindowEndTimeStamp) - .clientAuctions(clientAuctions) - .instanceId(deploymentProperties.getPbsHostId()) - .region(deploymentProperties.getPbsRegion()) - .vendor(deploymentProperties.getPbsVendor()) - .build()) - .collect(Collectors.toSet()); - - logNotDeliveredLineItems(deliveryProgress, reportsBatch); - return DeliveryProgressReportBatch.of(reportsBatch, reportId, dataWindowEndTimeStamp); - } - - private DeliveryProgressReport.DeliveryProgressReportBuilder updateReportWithLineItems( - DeliveryProgress deliveryProgress, - List lineItemStatuses, - Map overallLineItemStatuses, - int lineItemsCount, - int batchNumber, - int batchSize, - boolean isOverall) { - final int startBatchIndex = batchNumber * batchSize; - final int endBatchIndex = (batchNumber + 1) * batchSize; - final List batchList = - lineItemStatuses.subList(startBatchIndex, Math.min(endBatchIndex, lineItemsCount)); - return DeliveryProgressReport.builder() - .lineItemStatus(makeLineItemStatusReports(deliveryProgress, batchList, - overallLineItemStatuses, isOverall)); - } - - private Set makeLineItemStatusReports( - DeliveryProgress deliveryProgress, - List lineItemStatuses, - Map overallLineItemStatuses, - boolean isOverall) { - - return lineItemStatuses.stream() - .map(lineItemStatus -> toLineItemStatusReport(lineItemStatus, - overallLineItemStatuses != null - ? overallLineItemStatuses.get(lineItemStatus.getLineItemId()) - : null, - deliveryProgress, isOverall)) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - private static void logNotDeliveredLineItems(DeliveryProgress deliveryProgress, - Set reportsBatch) { - final Set reportedLineItems = reportsBatch.stream() - .map(DeliveryProgressReport::getLineItemStatus) - .flatMap(Collection::stream) - .map(LineItemStatus::getLineItemId) - .collect(Collectors.toSet()); - - final String notDeliveredLineItems = deliveryProgress.getLineItemStatuses().keySet().stream() - .filter(id -> !reportedLineItems.contains(id)) - .collect(Collectors.joining(", ")); - if (StringUtils.isNotBlank(notDeliveredLineItems)) { - logger.info("Line item with id {0} will not be reported," - + " as it does not have active delivery schedules during report window.", notDeliveredLineItems); - } - } - - DeliveryProgressReport updateReportTimeStamp(DeliveryProgressReport deliveryProgressReport, ZonedDateTime now) { - return deliveryProgressReport.toBuilder().reportTimeStamp(formatTimeStamp(now)).build(); - } - - private LineItemStatus toLineItemStatusReport(org.prebid.server.deals.lineitem.LineItemStatus lineItemStatus, - org.prebid.server.deals.lineitem.LineItemStatus overallLineItemStatus, - DeliveryProgress deliveryProgress, boolean isOverall) { - final String lineItemId = lineItemStatus.getLineItemId(); - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - if (isOverall && lineItem == null) { - return null; - } - final DeliveryPlan activeDeliveryPlan = ObjectUtil.getIfNotNull(lineItem, LineItem::getActiveDeliveryPlan); - final Set deliverySchedules = deliverySchedule(lineItemStatus, overallLineItemStatus, - activeDeliveryPlan); - if (CollectionUtils.isEmpty(deliverySchedules) && !isOverall) { - return null; - } - - return LineItemStatus.builder() - .lineItemSource(ObjectUtil.firstNonNull(lineItemStatus::getSource, - () -> ObjectUtil.getIfNotNull(lineItem, LineItem::getSource))) - .lineItemId(lineItemId) - .dealId(ObjectUtil.firstNonNull(lineItemStatus::getDealId, - () -> ObjectUtil.getIfNotNull(lineItem, LineItem::getDealId))) - .extLineItemId(ObjectUtil.firstNonNull(lineItemStatus::getExtLineItemId, - () -> ObjectUtil.getIfNotNull(lineItem, LineItem::getExtLineItemId))) - .accountAuctions(accountRequests(ObjectUtil.firstNonNull(lineItemStatus::getAccountId, - () -> ObjectUtil.getIfNotNull(lineItem, LineItem::getAccountId)), deliveryProgress)) - .domainMatched(lineItemStatus.getDomainMatched().sum()) - .targetMatched(lineItemStatus.getTargetMatched().sum()) - .targetMatchedButFcapped(lineItemStatus.getTargetMatchedButFcapped().sum()) - .targetMatchedButFcapLookupFailed(lineItemStatus.getTargetMatchedButFcapLookupFailed().sum()) - .pacingDeferred(lineItemStatus.getPacingDeferred().sum()) - .sentToBidder(lineItemStatus.getSentToBidder().sum()) - .sentToBidderAsTopMatch(lineItemStatus.getSentToBidderAsTopMatch().sum()) - .receivedFromBidder(lineItemStatus.getReceivedFromBidder().sum()) - .receivedFromBidderInvalidated(lineItemStatus.getReceivedFromBidderInvalidated().sum()) - .sentToClient(lineItemStatus.getSentToClient().sum()) - .sentToClientAsTopMatch(lineItemStatus.getSentToClientAsTopMatch().sum()) - .lostToLineItems(lostToLineItems(lineItemStatus, deliveryProgress)) - .events(lineItemStatus.getEvents()) - .deliverySchedule(deliverySchedules) - .readyAt(isOverall ? toReadyAt(lineItem) : null) - .spentTokens(isOverall && activeDeliveryPlan != null ? activeDeliveryPlan.getSpentTokens() : null) - .pacingFrequency(isOverall && activeDeliveryPlan != null - ? activeDeliveryPlan.getDeliveryRateInMilliseconds() - : null) - .build(); - } - - private String toReadyAt(LineItem lineItem) { - final ZonedDateTime readyAt = ObjectUtil.getIfNotNull(lineItem, LineItem::getReadyAt); - return readyAt != null ? UTC_MILLIS_FORMATTER.format(readyAt) : null; - } - - private Long accountRequests(String accountId, DeliveryProgress deliveryProgress) { - final LongAdder accountRequests = accountId != null - ? deliveryProgress.getRequestsPerAccount().get(accountId) - : null; - return accountRequests != null ? accountRequests.sum() : null; - } - - private Set lostToLineItems(org.prebid.server.deals.lineitem.LineItemStatus lineItemStatus, - DeliveryProgress deliveryProgress) { - final Map lostTo = - deliveryProgress.getLineItemIdToLost().get(lineItemStatus.getLineItemId()); - - if (lostTo != null) { - return lostTo.values().stream() - .sorted(LOST_TO_LINE_ITEM_COMPARATOR.reversed()) - .map(this::toLostToLineItems) - .limit(competitorsNumber) - .collect(Collectors.toSet()); - } - - return null; - } - - private LostToLineItem toLostToLineItems(org.prebid.server.deals.lineitem.LostToLineItem lostToLineItem) { - final String lineItemId = lostToLineItem.getLineItemId(); - return LostToLineItem.of( - ObjectUtil.getIfNotNull(lineItemService.getLineItemById(lineItemId), LineItem::getSource), lineItemId, - lostToLineItem.getCount().sum()); - } - - private static Set deliverySchedule( - org.prebid.server.deals.lineitem.LineItemStatus lineItemStatus, - org.prebid.server.deals.lineitem.LineItemStatus overallLineItemStatus, - DeliveryPlan activeDeliveryPlan) { - - final Map idToDeliveryPlan = overallLineItemStatus != null - ? overallLineItemStatus.getDeliveryPlans().stream() - .collect(Collectors.toMap(DeliveryPlan::getPlanId, Function.identity())) - : Collections.emptyMap(); - - final Set deliverySchedules = lineItemStatus.getDeliveryPlans().stream() - .map(deliveryPlan -> toDeliverySchedule(deliveryPlan, idToDeliveryPlan.get(deliveryPlan.getPlanId()))) - .collect(Collectors.toSet()); - - if (CollectionUtils.isEmpty(deliverySchedules)) { - if (activeDeliveryPlan != null) { - deliverySchedules.add(DeliveryProgressReportFactory - .toDeliverySchedule(activeDeliveryPlan.withoutSpentTokens())); - } - } - return deliverySchedules; - } - - static DeliverySchedule toDeliverySchedule(DeliveryPlan deliveryPlan) { - return toDeliverySchedule(deliveryPlan, null); - } - - private static DeliverySchedule toDeliverySchedule(DeliveryPlan plan, DeliveryPlan overallPlan) { - final Map priorityClassToTotalSpent = overallPlan != null - ? overallPlan.getDeliveryTokens().stream() - .collect(Collectors.toMap(DeliveryToken::getPriorityClass, deliveryToken -> deliveryToken.getSpent() - .sum())) - : Collections.emptyMap(); - - final Set tokens = plan.getDeliveryTokens().stream() - .map(token -> Token.of(token.getPriorityClass(), token.getTotal(), - token.getSpent().sum(), priorityClassToTotalSpent.get(token.getPriorityClass()))) - .collect(Collectors.toSet()); - - return DeliverySchedule.builder() - .planId(plan.getPlanId()) - .planStartTimeStamp(formatTimeStamp(plan.getStartTimeStamp())) - .planExpirationTimeStamp(formatTimeStamp(plan.getEndTimeStamp())) - .planUpdatedTimeStamp(formatTimeStamp(plan.getUpdatedTimeStamp())) - .tokens(tokens) - .build(); - } - - private static String formatTimeStamp(ZonedDateTime zonedDateTime) { - return zonedDateTime != null - ? UTC_MILLIS_FORMATTER.format(zonedDateTime) - : null; - } - - private static class LostToLineItemComparator implements - Comparator { - - @Override - public int compare(org.prebid.server.deals.lineitem.LostToLineItem lostToLineItem1, - org.prebid.server.deals.lineitem.LostToLineItem lostToLineItem2) { - return Long.compare(lostToLineItem1.getCount().sum(), lostToLineItem2.getCount().sum()); - } - } -} diff --git a/src/main/java/org/prebid/server/deals/DeliveryProgressService.java b/src/main/java/org/prebid/server/deals/DeliveryProgressService.java deleted file mode 100644 index ab65595ea47..00000000000 --- a/src/main/java/org/prebid/server/deals/DeliveryProgressService.java +++ /dev/null @@ -1,208 +0,0 @@ -package org.prebid.server.deals; - -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.deals.events.ApplicationEventProcessor; -import org.prebid.server.deals.lineitem.DeliveryPlan; -import org.prebid.server.deals.lineitem.DeliveryProgress; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.deals.lineitem.LineItemStatus; -import org.prebid.server.deals.model.DeliveryProgressProperties; -import org.prebid.server.deals.model.TxnLog; -import org.prebid.server.deals.proto.report.DeliveryProgressReport; -import org.prebid.server.deals.proto.report.DeliverySchedule; -import org.prebid.server.deals.proto.report.LineItemStatusReport; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.log.CriteriaLogManager; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -/** - * Tracks {@link LineItem}s' progress. - */ -public class DeliveryProgressService implements ApplicationEventProcessor { - - private static final Logger logger = LoggerFactory.getLogger(DeliveryProgressService.class); - - private final DeliveryProgressProperties deliveryProgressProperties; - private final LineItemService lineItemService; - private final DeliveryStatsService deliveryStatsService; - private final DeliveryProgressReportFactory deliveryProgressReportFactory; - private final Clock clock; - private final CriteriaLogManager criteriaLogManager; - - private final long lineItemStatusTtl; - - protected final DeliveryProgress overallDeliveryProgress; - protected DeliveryProgress currentDeliveryProgress; - - public DeliveryProgressService(DeliveryProgressProperties deliveryProgressProperties, - LineItemService lineItemService, - DeliveryStatsService deliveryStatsService, - DeliveryProgressReportFactory deliveryProgressReportFactory, - Clock clock, - CriteriaLogManager criteriaLogManager) { - this.deliveryProgressProperties = Objects.requireNonNull(deliveryProgressProperties); - this.lineItemService = Objects.requireNonNull(lineItemService); - this.deliveryStatsService = Objects.requireNonNull(deliveryStatsService); - this.deliveryProgressReportFactory = Objects.requireNonNull(deliveryProgressReportFactory); - this.clock = Objects.requireNonNull(clock); - this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); - - this.lineItemStatusTtl = TimeUnit.SECONDS.toMillis(deliveryProgressProperties.getLineItemStatusTtlSeconds()); - - final ZonedDateTime now = ZonedDateTime.now(clock); - overallDeliveryProgress = DeliveryProgress.of(now, lineItemService); - currentDeliveryProgress = DeliveryProgress.of(now, lineItemService); - } - - public void shutdown() { - createDeliveryProgressReports(ZonedDateTime.now(clock)); - deliveryStatsService.sendDeliveryProgressReports(); - } - - /** - * Updates copy of overall {@link DeliveryProgress} with current delivery progress and - * creates {@link DeliveryProgressReport}. - */ - public DeliveryProgressReport getOverallDeliveryProgressReport() { - final DeliveryProgress overallDeliveryProgressCopy = - overallDeliveryProgress.copyWithOriginalPlans(); - - lineItemService.getLineItems() - .forEach(lineItem -> overallDeliveryProgressCopy.getLineItemStatuses() - .putIfAbsent(lineItem.getLineItemId(), LineItemStatus.of(lineItem.getLineItemId(), - lineItem.getSource(), lineItem.getDealId(), lineItem.getExtLineItemId(), - lineItem.getAccountId()))); - - overallDeliveryProgressCopy.mergeFrom(currentDeliveryProgress); - return deliveryProgressReportFactory.fromDeliveryProgress(overallDeliveryProgressCopy, ZonedDateTime.now(clock), - true); - } - - /** - * Updates delivery progress from {@link AuctionContext} statistics. - */ - @Override - public void processAuctionEvent(AuctionContext auctionContext) { - processAuctionEvent(auctionContext.getTxnLog(), auctionContext.getAccount().getId(), ZonedDateTime.now(clock)); - } - - /** - * Updates delivery progress from {@link AuctionContext} statistics for defined date. - */ - protected void processAuctionEvent(TxnLog txnLog, String accountId, ZonedDateTime now) { - final Map planIdToTokenPriority = new HashMap<>(); - - txnLog.lineItemSentToClientAsTopMatch().stream() - .map(lineItemService::getLineItemById) - .filter(Objects::nonNull) - .filter(lineItem -> lineItem.getActiveDeliveryPlan() != null) - .forEach(lineItem -> incrementTokens(lineItem, now, planIdToTokenPriority)); - - currentDeliveryProgress.recordTransactionLog(txnLog, planIdToTokenPriority, accountId); - } - - /** - * Updates delivery progress with win event. - */ - @Override - public void processLineItemWinEvent(String lineItemId) { - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - if (lineItem != null) { - currentDeliveryProgress.recordWinEvent(lineItemId); - criteriaLogManager.log(logger, lineItem.getAccountId(), lineItem.getSource(), lineItemId, - "Win event for LineItem with id %s was recorded".formatted(lineItemId), logger::debug); - } - } - - @Override - public void processDeliveryProgressUpdateEvent() { - lineItemService.getLineItems() - .stream() - .filter(lineItem -> lineItem.getActiveDeliveryPlan() != null) - .forEach(this::mergePlanFromLineItem); - } - - private void mergePlanFromLineItem(LineItem lineItem) { - overallDeliveryProgress.upsertPlanReferenceFromLineItem(lineItem); - currentDeliveryProgress.mergePlanFromLineItem(lineItem); - } - - /** - * Prepare report from statuses to send it to delivery stats. - */ - public void createDeliveryProgressReports(ZonedDateTime now) { - final DeliveryProgress deliveryProgressToReport = currentDeliveryProgress; - - currentDeliveryProgress = DeliveryProgress.of(now, lineItemService); - - deliveryProgressToReport.setEndTimeStamp(now); - deliveryProgressToReport.updateWithActiveLineItems(lineItemService.getLineItems()); - - overallDeliveryProgress.mergeFrom(deliveryProgressToReport); - - deliveryStatsService.addDeliveryProgress(deliveryProgressToReport, - overallDeliveryProgress.getLineItemStatuses()); - - overallDeliveryProgress.cleanLineItemStatuses( - now, lineItemStatusTtl, deliveryProgressProperties.getCachedPlansNumber()); - } - - public void invalidateLineItemsByIds(List lineItemIds) { - overallDeliveryProgress.getLineItemStatuses().entrySet() - .removeIf(stringLineItemEntry -> lineItemIds.contains(stringLineItemEntry.getKey())); - currentDeliveryProgress.getLineItemStatuses().entrySet() - .removeIf(stringLineItemEntry -> lineItemIds.contains(stringLineItemEntry.getKey())); - } - - public void invalidateLineItems() { - overallDeliveryProgress.getLineItemStatuses().clear(); - currentDeliveryProgress.getLineItemStatuses().clear(); - } - - /** - * Increments tokens for specified in parameters lineItem, plan and class priority. - */ - protected void incrementTokens(LineItem lineItem, ZonedDateTime now, Map planIdToTokenPriority) { - final Integer classPriority = lineItem.incSpentToken(now); - if (classPriority != null) { - planIdToTokenPriority.put(lineItem.getActiveDeliveryPlan().getPlanId(), classPriority); - } - } - - /** - * Returns {@link LineItemStatusReport} for the given {@link LineItem}'s ID. - */ - public LineItemStatusReport getLineItemStatusReport(String lineItemId) { - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - if (lineItem == null) { - throw new PreBidException("LineItem not found: " + lineItemId); - } - - final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); - if (activeDeliveryPlan == null) { - return LineItemStatusReport.builder() - .lineItemId(lineItemId) - .build(); - } - - final DeliverySchedule deliverySchedule = DeliveryProgressReportFactory.toDeliverySchedule(activeDeliveryPlan); - return LineItemStatusReport.builder() - .lineItemId(lineItemId) - .deliverySchedule(deliverySchedule) - .readyToServeTimestamp(lineItem.getReadyAt()) - .spentTokens(activeDeliveryPlan.getSpentTokens()) - .pacingFrequency(activeDeliveryPlan.getDeliveryRateInMilliseconds()) - .accountId(lineItem.getAccountId()) - .target(lineItem.getTargeting()) - .build(); - } -} diff --git a/src/main/java/org/prebid/server/deals/DeliveryStatsService.java b/src/main/java/org/prebid/server/deals/DeliveryStatsService.java deleted file mode 100644 index 8fe8973b394..00000000000 --- a/src/main/java/org/prebid/server/deals/DeliveryStatsService.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.prebid.server.deals; - -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.http.HttpHeaders; -import org.prebid.server.deals.lineitem.DeliveryProgress; -import org.prebid.server.deals.lineitem.LineItemStatus; -import org.prebid.server.deals.model.AlertPriority; -import org.prebid.server.deals.model.DeliveryStatsProperties; -import org.prebid.server.deals.proto.report.DeliveryProgressReport; -import org.prebid.server.deals.proto.report.DeliveryProgressReportBatch; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.metric.Metrics; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Clock; -import java.time.ZonedDateTime; -import java.util.Base64; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Map; -import java.util.NavigableSet; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.zip.GZIPOutputStream; - -public class DeliveryStatsService implements Suspendable { - - private static final Logger logger = LoggerFactory.getLogger(DeliveryStatsService.class); - - private static final String BASIC_AUTH_PATTERN = "Basic %s"; - private static final String PG_TRX_ID = "pg-trx-id"; - private static final String PBS_DELIVERY_CLIENT_ERROR = "pbs-delivery-stats-client-error"; - private static final String SERVICE_NAME = "deliveryStats"; - public static final String GZIP = "gzip"; - - private final DeliveryStatsProperties deliveryStatsProperties; - private final DeliveryProgressReportFactory deliveryProgressReportFactory; - private final AlertHttpService alertHttpService; - private final HttpClient httpClient; - private final Metrics metrics; - private final Clock clock; - private final Vertx vertx; - private final JacksonMapper mapper; - - private final String basicAuthHeader; - private final NavigableSet requiredBatches; - private volatile boolean isSuspended; - - public DeliveryStatsService(DeliveryStatsProperties deliveryStatsProperties, - DeliveryProgressReportFactory deliveryProgressReportFactory, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - Vertx vertx, - JacksonMapper mapper) { - - this.deliveryStatsProperties = Objects.requireNonNull(deliveryStatsProperties); - this.deliveryProgressReportFactory = Objects.requireNonNull(deliveryProgressReportFactory); - this.alertHttpService = Objects.requireNonNull(alertHttpService); - this.httpClient = Objects.requireNonNull(httpClient); - this.clock = Objects.requireNonNull(clock); - this.vertx = Objects.requireNonNull(vertx); - this.metrics = Objects.requireNonNull(metrics); - this.mapper = Objects.requireNonNull(mapper); - this.basicAuthHeader = authHeader(deliveryStatsProperties.getUsername(), deliveryStatsProperties.getPassword()); - - requiredBatches = new ConcurrentSkipListSet<>(Comparator - .comparing(DeliveryProgressReportBatch::getDataWindowEndTimeStamp) - .thenComparing(DeliveryProgressReportBatch::hashCode)); - } - - @Override - public void suspend() { - isSuspended = true; - } - - public void addDeliveryProgress(DeliveryProgress deliveryProgress, - Map overallLineItemStatuses) { - requiredBatches.add(deliveryProgressReportFactory.batchFromDeliveryProgress(deliveryProgress, - overallLineItemStatuses, null, deliveryStatsProperties.getLineItemsPerReport(), false)); - } - - public void sendDeliveryProgressReports() { - sendDeliveryProgressReports(ZonedDateTime.now(clock)); - } - - public void sendDeliveryProgressReports(ZonedDateTime now) { - if (isSuspended) { - logger.warn("Report will not be sent, as service was suspended from register response"); - return; - } - final long batchesIntervalMs = deliveryStatsProperties.getBatchesIntervalMs(); - final int batchesCount = requiredBatches.size(); - final Set sentBatches = new HashSet<>(); - requiredBatches.stream() - .reduce(Future.succeededFuture(), - (future, batch) -> future.compose(v -> sendBatch(batch, now) - .map(aVoid -> sentBatches.add(batch)) - .compose(aVoid -> batchesIntervalMs > 0 && batchesCount > sentBatches.size() - ? setInterval(batchesIntervalMs) - : Future.succeededFuture())), - // combiner does not do any useful operations, just required for this type of reduce operation - (a, b) -> Promise.promise().future()) - .onComplete(result -> handleDeliveryResult(result, batchesCount, sentBatches)); - } - - protected Future sendBatch(DeliveryProgressReportBatch deliveryProgressReportBatch, ZonedDateTime now) { - final Promise promise = Promise.promise(); - final MultiMap headers = headers(); - final Set sentReports = new HashSet<>(); - final long reportIntervalMs = deliveryStatsProperties.getReportsIntervalMs(); - final Set reports = deliveryProgressReportBatch.getReports(); - final int reportsCount = reports.size(); - reports.stream() - .reduce(Future.succeededFuture(), - (future, report) -> future.compose(v -> sendReport(report, headers, now) - .map(aVoid -> sentReports.add(report))) - .compose(aVoid -> reportIntervalMs > 0 && reportsCount > sentReports.size() - ? setInterval(reportIntervalMs) - : Future.succeededFuture()), - (a, b) -> Promise.promise().future()) - .onComplete(result -> handleBatchDelivery(result, deliveryProgressReportBatch, sentReports, promise)); - return promise.future(); - } - - protected Future sendReport(DeliveryProgressReport deliveryProgressReport, MultiMap headers, - ZonedDateTime now) { - final Promise promise = Promise.promise(); - final long startTime = clock.millis(); - if (isSuspended) { - logger.warn("Report will not be sent, as service was suspended from register response"); - promise.complete(); - return promise.future(); - } - - final String body = mapper.encodeToString(deliveryProgressReportFactory - .updateReportTimeStamp(deliveryProgressReport, now)); - - logger.info("Sending delivery progress report to Delivery Stats, {0} is {1}", PG_TRX_ID, - headers.get(PG_TRX_ID)); - logger.debug("Delivery progress report is: {0}", body); - if (deliveryStatsProperties.isRequestCompressionEnabled()) { - headers.add(HttpHeaders.CONTENT_ENCODING, GZIP); - httpClient.request(HttpMethod.POST, deliveryStatsProperties.getEndpoint(), headers, gzipBody(body), - deliveryStatsProperties.getTimeoutMs()) - .onComplete(result -> handleDeliveryProgressReport(result, deliveryProgressReport, promise, - startTime)); - } else { - httpClient.post(deliveryStatsProperties.getEndpoint(), headers, body, - deliveryStatsProperties.getTimeoutMs()) - .onComplete(result -> handleDeliveryProgressReport(result, deliveryProgressReport, promise, - startTime)); - } - - return promise.future(); - } - - /** - * Handles delivery report response from Planner. - */ - private void handleDeliveryProgressReport(AsyncResult result, - DeliveryProgressReport deliveryProgressReport, - Promise promise, - long startTime) { - metrics.updateRequestTimeMetric(MetricName.delivery_request_time, clock.millis() - startTime); - if (result.failed()) { - logger.warn("Cannot send delivery progress report to delivery stats service", result.cause()); - promise.fail(new PreBidException("Sending report with id = %s failed in a reason: %s" - .formatted(deliveryProgressReport.getReportId(), result.cause().getMessage()))); - } else { - final int statusCode = result.result().getStatusCode(); - final String reportId = deliveryProgressReport.getReportId(); - if (statusCode == 200 || statusCode == 409) { - handleSuccessfulResponse(deliveryProgressReport, promise, statusCode, reportId); - } else { - logger.warn("HTTP status code {0}", statusCode); - promise.fail(new PreBidException( - "Delivery stats service responded with status code = %s for report with id = %s" - .formatted(statusCode, deliveryProgressReport.getReportId()))); - } - } - } - - private void handleSuccessfulResponse(DeliveryProgressReport deliveryProgressReport, Promise promise, - int statusCode, String reportId) { - metrics.updateDeliveryRequestMetric(true); - promise.complete(); - if (statusCode == 409) { - logger.info("Delivery stats service respond with 409 duplicated, report with {0} line items and id = {1}" - + " was already delivered before and will be removed from from delivery queue", - deliveryProgressReport.getLineItemStatus().size(), reportId); - } else { - logger.info("Delivery progress report with {0} line items and id = {1} was successfully sent to" - + " delivery stats service", deliveryProgressReport.getLineItemStatus().size(), reportId); - } - } - - private Future setInterval(long interval) { - final Promise promise = Promise.promise(); - vertx.setTimer(interval, event -> promise.complete()); - return promise.future(); - } - - private void handleDeliveryResult(AsyncResult result, int reportBatchesNumber, - Set sentBatches) { - if (result.failed()) { - logger.warn("Failed to send {0} report batches, {1} report batches left to send." - + " Reason is: {2}", reportBatchesNumber, reportBatchesNumber - sentBatches.size(), - result.cause().getMessage()); - alertHttpService.alertWithPeriod( - SERVICE_NAME, - PBS_DELIVERY_CLIENT_ERROR, - AlertPriority.MEDIUM, - "Report was not send to delivery stats service with a reason: " + result.cause().getMessage()); - requiredBatches.removeAll(sentBatches); - handleFailedReportDelivery(); - } else { - requiredBatches.clear(); - alertHttpService.resetAlertCount(PBS_DELIVERY_CLIENT_ERROR); - logger.info("{0} report batches were successfully sent.", reportBatchesNumber); - } - } - - private void handleBatchDelivery(AsyncResult result, - DeliveryProgressReportBatch deliveryProgressReportBatch, - Set sentReports, - Promise promise) { - final String reportId = deliveryProgressReportBatch.getReportId(); - final String endTimeWindow = deliveryProgressReportBatch.getDataWindowEndTimeStamp(); - final int batchSize = deliveryProgressReportBatch.getReports().size(); - final int sentSize = sentReports.size(); - if (result.succeeded()) { - logger.info("Batch of reports with reports id = {0}, end time window = {1} and size {2} was successfully" - + " sent", reportId, endTimeWindow, batchSize); - promise.complete(); - } else { - logger.warn("Failed to sent batch of reports with reports id = {0} end time windows = {1}." - + " {2} out of {3} were sent.", reportId, endTimeWindow, sentSize, batchSize); - deliveryProgressReportBatch.removeReports(sentReports); - promise.fail(result.cause().getMessage()); - } - } - - protected MultiMap headers() { - return MultiMap.caseInsensitiveMultiMap() - .add(HttpUtil.AUTHORIZATION_HEADER, basicAuthHeader) - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) - .add(PG_TRX_ID, UUID.randomUUID().toString()); - } - - /** - * Creates Authorization header value from username and password. - */ - private static String authHeader(String username, String password) { - return BASIC_AUTH_PATTERN - .formatted(Base64.getEncoder().encodeToString((username + ':' + password).getBytes())); - } - - private static byte[] gzipBody(String body) { - try ( - ByteArrayOutputStream obj = new ByteArrayOutputStream(); - GZIPOutputStream gzip = new GZIPOutputStream(obj)) { - gzip.write(body.getBytes(StandardCharsets.UTF_8)); - gzip.finish(); - return obj.toByteArray(); - } catch (IOException e) { - throw new PreBidException("Failed to gzip request with a reason : " + e.getMessage()); - } - } - - private void handleFailedReportDelivery() { - metrics.updateDeliveryRequestMetric(false); - while (requiredBatches.size() > deliveryStatsProperties.getCachedReportsNumber()) { - requiredBatches.pollFirst(); - } - } -} diff --git a/src/main/java/org/prebid/server/deals/LineItemService.java b/src/main/java/org/prebid/server/deals/LineItemService.java deleted file mode 100644 index 1fafdadfa9b..00000000000 --- a/src/main/java/org/prebid/server/deals/LineItemService.java +++ /dev/null @@ -1,591 +0,0 @@ -package org.prebid.server.deals; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.events.ApplicationEventService; -import org.prebid.server.deals.lineitem.DeliveryPlan; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.deals.model.MatchLineItemsResult; -import org.prebid.server.deals.model.TxnLog; -import org.prebid.server.deals.proto.DeliverySchedule; -import org.prebid.server.deals.proto.LineItemMetaData; -import org.prebid.server.deals.proto.Price; -import org.prebid.server.deals.targeting.TargetingDefinition; -import org.prebid.server.exception.TargetingSyntaxException; -import org.prebid.server.log.CriteriaLogManager; -import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; -import org.prebid.server.util.HttpUtil; - -import java.math.BigDecimal; -import java.time.Clock; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * Works with {@link LineItem} related information. - */ -public class LineItemService { - - private static final Logger logger = LoggerFactory.getLogger(LineItemService.class); - - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - - private static final String ACTIVE = "active"; - private static final String PG_IGNORE_PACING_VALUE = "1"; - - private final Comparator lineItemComparator = Comparator - .comparing(LineItem::getHighestUnspentTokensClass, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(LineItem::getRelativePriority, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(LineItem::getCpm, Comparator.nullsLast(Comparator.reverseOrder())); - - private final int maxDealsPerBidder; - private final TargetingService targetingService; - private final CurrencyConversionService conversionService; - protected final ApplicationEventService applicationEventService; - private final String adServerCurrency; - private final Clock clock; - private final CriteriaLogManager criteriaLogManager; - - protected final Map idToLineItems; - protected volatile boolean isPlannerResponsive; - - public LineItemService(int maxDealsPerBidder, - TargetingService targetingService, - CurrencyConversionService conversionService, - ApplicationEventService applicationEventService, - String adServerCurrency, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - this.maxDealsPerBidder = maxDealsPerBidder; - this.targetingService = Objects.requireNonNull(targetingService); - this.conversionService = Objects.requireNonNull(conversionService); - this.applicationEventService = Objects.requireNonNull(applicationEventService); - this.adServerCurrency = Objects.requireNonNull(adServerCurrency); - this.clock = Objects.requireNonNull(clock); - this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); - - idToLineItems = new ConcurrentHashMap<>(); - } - - /** - * Returns {@link LineItem} by its id. - */ - public LineItem getLineItemById(String lineItemId) { - return idToLineItems.get(lineItemId); - } - - /** - * Returns true when account has at least one active {@link LineItem}. - */ - public boolean accountHasDeals(AuctionContext auctionContext) { - return accountHasDeals(auctionContext.getAccount().getId(), ZonedDateTime.now(clock)); - } - - /** - * Returns true when account has at least one active {@link LineItem} in the given time. - */ - public boolean accountHasDeals(String account, ZonedDateTime now) { - return StringUtils.isNotEmpty(account) - && idToLineItems.values().stream().anyMatch(lineItem -> Objects.equals(lineItem.getAccountId(), account) - && lineItem.isActive(now)); - } - - /** - * Finds among active Line Items those matching Imp of the OpenRTB2 request - * taking into account Line Items’ targeting and delivery progress. - */ - public MatchLineItemsResult findMatchingLineItems(BidRequest bidRequest, - Imp imp, - String bidder, - BidderAliases aliases, - AuctionContext auctionContext) { - - final ZonedDateTime now = ZonedDateTime.now(clock); - return findMatchingLineItems(bidRequest, imp, bidder, aliases, auctionContext, now); - } - - /** - * Finds among active Line Items those matching Imp of the OpenRTB2 request - * taking into account Line Items’ targeting and delivery progress by the given time. - */ - protected MatchLineItemsResult findMatchingLineItems(BidRequest bidRequest, - Imp imp, - String bidder, - BidderAliases aliases, - AuctionContext auctionContext, - ZonedDateTime now) { - - final List matchedLineItems = - getPreMatchedLineItems(auctionContext.getAccount().getId(), bidder, aliases).stream() - .filter(lineItem -> isTargetingMatched(lineItem, bidRequest, imp, auctionContext)) - .toList(); - - return MatchLineItemsResult.of( - postProcessMatchedLineItems(matchedLineItems, bidRequest, imp, auctionContext, now)); - } - - public void updateIsPlannerResponsive(boolean isPlannerResponsive) { - this.isPlannerResponsive = isPlannerResponsive; - } - - /** - * Updates metadata, starts tracking new {@link LineItem}s and {@link DeliverySchedule}s - * and remove from tracking expired. - */ - public void updateLineItems(List planResponse, boolean isPlannerResponsive) { - updateLineItems(planResponse, isPlannerResponsive, ZonedDateTime.now(clock)); - } - - public void updateLineItems(List planResponse, boolean isPlannerResponsive, ZonedDateTime now) { - this.isPlannerResponsive = isPlannerResponsive; - if (isPlannerResponsive) { - final List lineItemsMetaData = ListUtils.emptyIfNull(planResponse).stream() - .filter(lineItemMetaData -> !isExpired(now, lineItemMetaData.getEndTimeStamp())) - .filter(lineItemMetaData -> Objects.equals(lineItemMetaData.getStatus(), ACTIVE)) - .toList(); - - removeInactiveLineItems(planResponse, now); - lineItemsMetaData.forEach(lineItemMetaData -> updateLineItem(lineItemMetaData, now)); - } - } - - public void invalidateLineItemsByIds(List lineItemIds) { - idToLineItems.entrySet().removeIf(stringLineItemEntry -> lineItemIds.contains(stringLineItemEntry.getKey())); - logger.info("Line Items with ids {0} were removed", String.join(", ", lineItemIds)); - } - - public void invalidateLineItems() { - final String lineItemsToRemove = String.join(", ", idToLineItems.keySet()); - idToLineItems.clear(); - logger.info("Line Items with ids {0} were removed", lineItemsToRemove); - } - - private boolean isExpired(ZonedDateTime now, ZonedDateTime endTime) { - return now.isAfter(endTime); - } - - private void removeInactiveLineItems(List planResponse, ZonedDateTime now) { - final Set lineItemsToRemove = ListUtils.emptyIfNull(planResponse).stream() - .filter(lineItemMetaData -> !Objects.equals(lineItemMetaData.getStatus(), ACTIVE) - || isExpired(now, lineItemMetaData.getEndTimeStamp())) - .map(LineItemMetaData::getLineItemId) - .collect(Collectors.toSet()); - - idToLineItems.entrySet().stream() - .filter(entry -> isExpired(now, entry.getValue().getEndTimeStamp())) - .map(Map.Entry::getKey) - .collect(Collectors.toCollection(() -> lineItemsToRemove)); - - if (CollectionUtils.isNotEmpty(lineItemsToRemove)) { - logger.info("Line Items {0} were dropped as expired or inactive", String.join(", ", lineItemsToRemove)); - } - idToLineItems.entrySet().removeIf(entry -> lineItemsToRemove.contains(entry.getKey())); - } - - protected Collection getLineItems() { - return idToLineItems.values(); - } - - protected void updateLineItem(LineItemMetaData lineItemMetaData, ZonedDateTime now) { - final TargetingDefinition targetingDefinition = makeTargeting(lineItemMetaData); - final Price normalizedPrice = normalizedPrice(lineItemMetaData); - - idToLineItems.compute(lineItemMetaData.getLineItemId(), (id, li) -> li != null - ? li.withUpdatedMetadata(lineItemMetaData, normalizedPrice, targetingDefinition, li.getReadyAt(), now) - : LineItem.of(lineItemMetaData, normalizedPrice, targetingDefinition, now)); - } - - public void advanceToNextPlan(ZonedDateTime now) { - final Collection lineItems = idToLineItems.values(); - for (LineItem lineItem : lineItems) { - lineItem.advanceToNextPlan(now, isPlannerResponsive); - } - applicationEventService.publishDeliveryUpdateEvent(); - } - - /** - * Creates {@link TargetingDefinition} from {@link LineItemMetaData} targeting json node. - */ - private TargetingDefinition makeTargeting(LineItemMetaData lineItemMetaData) { - TargetingDefinition targetingDefinition; - try { - targetingDefinition = targetingService.parseTargetingDefinition(lineItemMetaData.getTargeting(), - lineItemMetaData.getLineItemId()); - } catch (TargetingSyntaxException e) { - criteriaLogManager.log( - logger, - lineItemMetaData.getAccountId(), - lineItemMetaData.getSource(), - lineItemMetaData.getLineItemId(), - "Line item targeting parsing failed with a reason: " + e.getMessage(), - logger::warn); - targetingDefinition = null; - } - return targetingDefinition; - } - - /** - * Returns {@link Price} with converted lineItem cpm to adServerCurrency. - */ - private Price normalizedPrice(LineItemMetaData lineItemMetaData) { - final Price price = lineItemMetaData.getPrice(); - if (price == null) { - return null; - } - - final String receivedCur = price.getCurrency(); - if (StringUtils.equals(adServerCurrency, receivedCur)) { - return price; - } - final BigDecimal updatedCpm = conversionService - .convertCurrency(price.getCpm(), Collections.emptyMap(), receivedCur, adServerCurrency, null); - - return Price.of(updatedCpm, adServerCurrency); - } - - private List getPreMatchedLineItems(String accountId, String bidder, BidderAliases aliases) { - if (StringUtils.isBlank(accountId)) { - return Collections.emptyList(); - } - - final List accountsLineItems = idToLineItems.values().stream() - .filter(lineItem -> lineItem.getAccountId().equals(accountId)) - .toList(); - - if (accountsLineItems.isEmpty()) { - criteriaLogManager.log( - logger, - accountId, - "There are no line items for account " + accountId, - logger::debug); - return Collections.emptyList(); - } - - return accountsLineItems.stream() - .filter(lineItem -> aliases.isSame(bidder, lineItem.getSource())) - .toList(); - } - - /** - * Returns true if {@link LineItem}s {@link TargetingDefinition} matches to {@link Imp}. - *

- * Updates deep debug log with matching information. - */ - private boolean isTargetingMatched(LineItem lineItem, - BidRequest bidRequest, - Imp imp, - AuctionContext auctionContext) { - - final TargetingDefinition targetingDefinition = lineItem.getTargetingDefinition(); - final String accountId = auctionContext.getAccount().getId(); - final String source = lineItem.getSource(); - final String lineItemId = lineItem.getLineItemId(); - if (targetingDefinition == null) { - deepDebug( - auctionContext, - Category.targeting, - "Line Item %s targeting was not defined or has incorrect format".formatted(lineItemId), - accountId, - source, - lineItemId); - return false; - } - - final boolean matched = targetingService.matchesTargeting( - bidRequest, imp, lineItem.getTargetingDefinition(), auctionContext); - - final String debugMessage = matched - ? "Line Item %s targeting matched imp with id %s".formatted(lineItemId, imp.getId()) - : "Line Item %s targeting did not match imp with id %s".formatted(lineItemId, imp.getId()); - deepDebug( - auctionContext, - Category.targeting, - debugMessage, - accountId, - source, - lineItemId); - - return matched; - } - - /** - * Filters {@link LineItem}s by next parameters: fcaps, readyAt, limit per bidder, same deal line items. - */ - private List postProcessMatchedLineItems(List lineItems, - BidRequest bidRequest, - Imp imp, - AuctionContext auctionContext, - ZonedDateTime now) { - - final TxnLog txnLog = auctionContext.getTxnLog(); - final List fcapIds = bidRequest.getUser().getExt().getFcapIds(); - - return lineItems.stream() - .peek(lineItem -> txnLog.lineItemsMatchedWholeTargeting().add(lineItem.getLineItemId())) - .filter(lineItem -> isNotFrequencyCapped(fcapIds, lineItem, auctionContext, txnLog)) - .filter(lineItem -> planHasTokensIfPresent(lineItem, auctionContext)) - .filter(lineItem -> isReadyAtInPast(now, lineItem, auctionContext, txnLog)) - .peek(lineItem -> txnLog.lineItemsReadyToServe().add(lineItem.getLineItemId())) - .collect(Collectors.groupingBy(lineItem -> lineItem.getSource().toLowerCase())) - .values().stream() - .map(valueAsLineItems -> filterLineItemPerBidder(valueAsLineItems, auctionContext, imp)) - .filter(CollectionUtils::isNotEmpty) - .peek(lineItemsForBidder -> recordInTxnSentToBidderAsTopMatch(txnLog, lineItemsForBidder)) - .flatMap(Collection::stream) - .peek(lineItem -> txnLog.lineItemsSentToBidder().get(lineItem.getSource()) - .add(lineItem.getLineItemId())) - .toList(); - } - - private boolean planHasTokensIfPresent(LineItem lineItem, AuctionContext auctionContext) { - if (hasUnspentTokens(lineItem) || ignorePacing(auctionContext)) { - return true; - } - - final String lineItemId = lineItem.getLineItemId(); - final String lineItemSource = lineItem.getSource(); - auctionContext.getTxnLog().lineItemsPacingDeferred().add(lineItemId); - deepDebug( - auctionContext, - Category.pacing, - "Matched Line Item %s for bidder %s does not have unspent tokens to be served" - .formatted(lineItemId, lineItemSource), - auctionContext.getAccount().getId(), - lineItemSource, - lineItemId); - - return false; - } - - private boolean hasUnspentTokens(LineItem lineItem) { - final DeliveryPlan deliveryPlan = lineItem.getActiveDeliveryPlan(); - return deliveryPlan == null || deliveryPlan.getDeliveryTokens().stream() - .anyMatch(deliveryToken -> deliveryToken.getUnspent() > 0); - } - - private static boolean ignorePacing(AuctionContext auctionContext) { - return PG_IGNORE_PACING_VALUE - .equals(auctionContext.getHttpRequest().getHeaders().get(HttpUtil.PG_IGNORE_PACING)); - } - - private boolean isReadyAtInPast(ZonedDateTime now, - LineItem lineItem, - AuctionContext auctionContext, - TxnLog txnLog) { - - final ZonedDateTime readyAt = lineItem.getReadyAt(); - final boolean ready = (readyAt != null && isBeforeOrEqual(readyAt, now)) || ignorePacing(auctionContext); - final String accountId = auctionContext.getAccount().getId(); - final String lineItemSource = lineItem.getSource(); - final String lineItemId = lineItem.getLineItemId(); - - if (ready) { - deepDebug( - auctionContext, - Category.pacing, - "Matched Line Item %s for bidder %s ready to serve. relPriority %d" - .formatted(lineItemId, lineItemSource, lineItem.getRelativePriority()), - accountId, - lineItemSource, - lineItemId); - } else { - txnLog.lineItemsPacingDeferred().add(lineItemId); - deepDebug( - auctionContext, - Category.pacing, - "Matched Line Item %s for bidder %s not ready to serve. Will be ready at %s, current time is %s" - .formatted( - lineItemId, - lineItemSource, - readyAt != null ? UTC_MILLIS_FORMATTER.format(readyAt) : "never", - UTC_MILLIS_FORMATTER.format(now)), - accountId, - lineItemSource, - lineItemId); - } - - return ready; - } - - private static boolean isBeforeOrEqual(ZonedDateTime before, ZonedDateTime after) { - return before.isBefore(after) || before.isEqual(after); - } - - /** - * Returns false if {@link LineItem} has fcaps defined and either - * - one of them present in the list of fcaps reached - * - list of fcaps reached is null which means that calling User Data Store failed - *

- * Otherwise returns true - *

- * Has side effect - records discarded line item id in the transaction log - */ - private boolean isNotFrequencyCapped(List frequencyCappedByIds, - LineItem lineItem, - AuctionContext auctionContext, - TxnLog txnLog) { - - if (CollectionUtils.isEmpty(lineItem.getFcapIds())) { - return true; - } - - final String lineItemId = lineItem.getLineItemId(); - final String accountId = auctionContext.getAccount().getId(); - final String lineItemSource = lineItem.getSource(); - - if (frequencyCappedByIds == null) { - txnLog.lineItemsMatchedTargetingFcapLookupFailed().add(lineItemId); - final String message = """ - Failed to match fcap for Line Item %s bidder %s in a reason of bad \ - response from user data service""".formatted(lineItemId, lineItemSource); - deepDebug(auctionContext, Category.pacing, message, accountId, lineItemSource, lineItemId); - criteriaLogManager.log( - logger, - lineItem.getAccountId(), - lineItem.getSource(), - lineItemId, - "Failed to match fcap for lineItem %s in a reason of bad response from user data service" - .formatted(lineItemId), - logger::debug); - - return false; - } else if (!frequencyCappedByIds.isEmpty()) { - final Optional fcapIdOptional = lineItem.getFcapIds().stream() - .filter(frequencyCappedByIds::contains).findFirst(); - if (fcapIdOptional.isPresent()) { - final String fcapId = fcapIdOptional.get(); - txnLog.lineItemsMatchedTargetingFcapped().add(lineItemId); - final String message = "Matched Line Item %s for bidder %s is frequency capped by fcap id %s." - .formatted(lineItemId, lineItemSource, fcapId); - deepDebug(auctionContext, Category.pacing, message, accountId, lineItemSource, lineItemId); - criteriaLogManager.log( - logger, lineItem.getAccountId(), lineItem.getSource(), lineItemId, message, logger::debug); - return false; - } - } - - return true; - } - - /** - * Filters {@link LineItem} with the same deal id and cuts {@link List} by maxDealsPerBidder value. - */ - private List filterLineItemPerBidder(List lineItems, AuctionContext auctionContext, Imp imp) { - final List sortedLineItems = new ArrayList<>(lineItems); - Collections.shuffle(sortedLineItems); - sortedLineItems.sort(lineItemComparator); - - final List filteredLineItems = uniqueBySentToBidderAsTopMatch(sortedLineItems, auctionContext, imp); - updateLostToLineItems(filteredLineItems, auctionContext.getTxnLog()); - - final Set dealIds = new HashSet<>(); - final List resolvedLineItems = new ArrayList<>(); - for (final LineItem lineItem : filteredLineItems) { - final String dealId = lineItem.getDealId(); - if (!dealIds.contains(dealId)) { - dealIds.add(dealId); - resolvedLineItems.add(lineItem); - } - } - return resolvedLineItems.size() > maxDealsPerBidder - ? cutLineItemsToDealMaxNumber(resolvedLineItems) - : resolvedLineItems; - } - - /** - * Removes from consideration any line items that have already been sent to bidder as the TopMatch - * in a previous impression for auction. - */ - private List uniqueBySentToBidderAsTopMatch(List lineItems, - AuctionContext auctionContext, - Imp imp) { - - final TxnLog txnLog = auctionContext.getTxnLog(); - final Set topMatchedLineItems = txnLog.lineItemsSentToBidderAsTopMatch().values().stream() - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - - final List result = new ArrayList<>(lineItems); - for (LineItem lineItem : lineItems) { - final String lineItemId = lineItem.getLineItemId(); - if (!topMatchedLineItems.contains(lineItemId)) { - return result; - } - result.remove(lineItem); - deepDebug( - auctionContext, - Category.cleanup, - "LineItem %s was dropped from imp with id %s because it was top match in another imp" - .formatted(lineItemId, imp.getId()), - auctionContext.getAccount().getId(), - lineItem.getSource(), - lineItemId); - } - return result; - } - - private List cutLineItemsToDealMaxNumber(List resolvedLineItems) { - resolvedLineItems.subList(maxDealsPerBidder, resolvedLineItems.size()) - .forEach(lineItem -> criteriaLogManager.log( - logger, - lineItem.getAccountId(), - lineItem.getSource(), - lineItem.getLineItemId(), - "LineItem %s was dropped by max deal per bidder limit %s" - .formatted(lineItem.getLineItemId(), maxDealsPerBidder), - logger::debug)); - return resolvedLineItems.subList(0, maxDealsPerBidder); - } - - private void updateLostToLineItems(List lineItems, TxnLog txnLog) { - for (int i = 1; i < lineItems.size(); i++) { - final LineItem lineItem = lineItems.get(i); - final Set lostTo = lineItems.subList(0, i).stream() - .map(LineItem::getLineItemId) - .collect(Collectors.toSet()); - txnLog.lostMatchingToLineItems().put(lineItem.getLineItemId(), lostTo); - } - } - - private void deepDebug(AuctionContext auctionContext, - Category category, - String message, - String accountId, - String bidder, - String lineItemId) { - - criteriaLogManager.log(logger, accountId, bidder, lineItemId, message, logger::debug); - auctionContext.getDeepDebugLog().add(lineItemId, category, () -> message); - } - - private static void recordInTxnSentToBidderAsTopMatch(TxnLog txnLog, List lineItemsForBidder) { - final LineItem topLineItem = lineItemsForBidder.get(0); - txnLog.lineItemsSentToBidderAsTopMatch() - .get(topLineItem.getSource()) - .add(topLineItem.getLineItemId()); - } -} diff --git a/src/main/java/org/prebid/server/deals/PlannerService.java b/src/main/java/org/prebid/server/deals/PlannerService.java deleted file mode 100644 index e719ad314f2..00000000000 --- a/src/main/java/org/prebid/server/deals/PlannerService.java +++ /dev/null @@ -1,239 +0,0 @@ -package org.prebid.server.deals; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.deals.model.AlertPriority; -import org.prebid.server.deals.model.DeploymentProperties; -import org.prebid.server.deals.model.PlannerProperties; -import org.prebid.server.deals.proto.LineItemMetaData; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.metric.Metrics; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.time.Clock; -import java.util.Base64; -import java.util.List; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Class manages line item metadata retrieving from planner and reporting. - */ -public class PlannerService implements Suspendable { - - private static final Logger logger = LoggerFactory.getLogger(PlannerService.class); - - protected static final TypeReference> LINE_ITEM_METADATA_TYPE_REFERENCE = - new TypeReference<>() { - }; - - private static final String BASIC_AUTH_PATTERN = "Basic %s"; - private static final String PG_TRX_ID = "pg-trx-id"; - private static final String INSTANCE_ID_PARAMETER = "instanceId"; - private static final String REGION_PARAMETER = "region"; - private static final String VENDOR_PARAMETER = "vendor"; - private static final String SERVICE_NAME = "planner"; - private static final String PBS_PLANNER_CLIENT_ERROR = "pbs-planner-client-error"; - private static final String PBS_PLANNER_EMPTY_RESPONSE = "pbs-planner-empty-response-error"; - - private final LineItemService lineItemService; - private final DeliveryProgressService deliveryProgressService; - private final AlertHttpService alertHttpService; - protected final HttpClient httpClient; - private final Metrics metrics; - private final Clock clock; - private final JacksonMapper mapper; - - protected final String planEndpoint; - private final long plannerTimeout; - private final String basicAuthHeader; - - protected final AtomicBoolean isPlannerResponsive; - private volatile boolean isSuspended; - - public PlannerService(PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - LineItemService lineItemService, - DeliveryProgressService deliveryProgressService, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - JacksonMapper mapper) { - this.lineItemService = Objects.requireNonNull(lineItemService); - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.alertHttpService = Objects.requireNonNull(alertHttpService); - this.httpClient = Objects.requireNonNull(httpClient); - this.metrics = Objects.requireNonNull(metrics); - this.clock = Objects.requireNonNull(clock); - this.mapper = Objects.requireNonNull(mapper); - - this.planEndpoint = buildPlannerMetaDataUrl(plannerProperties.getPlanEndpoint(), - deploymentProperties.getPbsHostId(), - deploymentProperties.getPbsRegion(), - deploymentProperties.getPbsVendor()); - this.plannerTimeout = plannerProperties.getTimeoutMs(); - this.basicAuthHeader = authHeader(plannerProperties.getUsername(), plannerProperties.getPassword()); - - this.isPlannerResponsive = new AtomicBoolean(true); - } - - @Override - public void suspend() { - isSuspended = true; - } - - /** - * Fetches line items meta data from Planner - */ - protected Future> fetchLineItemMetaData(String plannerUrl, MultiMap headers) { - logger.info("Requesting line items metadata and plans from Planner, {0} is {1}", PG_TRX_ID, - headers.get(PG_TRX_ID)); - final long startTime = clock.millis(); - return httpClient.get(plannerUrl, headers, plannerTimeout) - .map(httpClientResponse -> processLineItemMetaDataResponse(httpClientResponse, startTime)); - } - - protected MultiMap headers() { - return MultiMap.caseInsensitiveMultiMap() - .add(HttpUtil.AUTHORIZATION_HEADER, basicAuthHeader) - .add(PG_TRX_ID, UUID.randomUUID().toString()); - } - - /** - * Processes response from planner. - * If status code == 4xx - stop fetching process. - * If status code =! 2xx - start retry fetching process. - * If status code == 200 - parse response. - */ - protected List processLineItemMetaDataResponse(HttpClientResponse response, long startTime) { - final int statusCode = response.getStatusCode(); - if (statusCode != 200) { - throw new PreBidException("Failed to fetch data from Planner, HTTP status code " + statusCode); - } - - final String body = response.getBody(); - if (body == null) { - throw new PreBidException("Failed to fetch data from planner, response can't be null"); - } - - metrics.updateRequestTimeMetric(MetricName.planner_request_time, clock.millis() - startTime); - - logger.debug("Received line item metadata and plans from Planner: {0}", body); - - try { - final List lineItemMetaData = mapper.decodeValue(body, - LINE_ITEM_METADATA_TYPE_REFERENCE); - validateForEmptyResponse(lineItemMetaData); - metrics.updateLineItemsNumberMetric(lineItemMetaData.size()); - logger.info("Received line item metadata from Planner, amount: {0}", lineItemMetaData.size()); - - return lineItemMetaData; - } catch (DecodeException e) { - final String errorMessage = "Cannot parse response: " + body; - throw new PreBidException(errorMessage, e); - } - } - - private void validateForEmptyResponse(List lineItemMetaData) { - if (CollectionUtils.isEmpty(lineItemMetaData)) { - alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_PLANNER_EMPTY_RESPONSE, AlertPriority.LOW, - "Response without line items was received from planner"); - } else { - alertHttpService.resetAlertCount(PBS_PLANNER_EMPTY_RESPONSE); - } - } - - /** - * Creates Authorization header value from username and password. - */ - private static String authHeader(String username, String password) { - return BASIC_AUTH_PATTERN - .formatted(Base64.getEncoder().encodeToString((username + ':' + password).getBytes())); - } - - /** - * Builds url for fetching metadata from planner - */ - private static String buildPlannerMetaDataUrl(String plannerMetaDataUrl, String pbsHostname, String pbsRegion, - String pbsVendor) { - return "%s?%s=%s&%s=%s&%s=%s".formatted( - plannerMetaDataUrl, - INSTANCE_ID_PARAMETER, - pbsHostname, - REGION_PARAMETER, - pbsRegion, - VENDOR_PARAMETER, - pbsVendor); - } - - /** - * Fetches line item metadata from planner during the regular, not retry flow. - */ - public void updateLineItemMetaData() { - if (isSuspended) { - logger.warn("Fetch request was not sent to general planner, as planner service is suspended from" - + " register endpoint."); - return; - } - - final MultiMap headers = headers(); - fetchLineItemMetaData(planEndpoint, headers) - .recover(ignored -> startRecoveryProcess(planEndpoint, headers)) - .onComplete(this::handleInitializationResult); - } - - private Future> startRecoveryProcess(String planEndpoint, MultiMap headers) { - metrics.updatePlannerRequestMetric(false); - logger.info("Retry to fetch line items from general planner by uri = {0}", planEndpoint); - - return fetchLineItemMetaData(planEndpoint, headers); - } - - /** - * Handles result of initialization process. Sets metadata if request was successful. - */ - protected void handleInitializationResult(AsyncResult> plannerResponse) { - if (plannerResponse.succeeded()) { - handleSuccessInitialization(plannerResponse); - } else { - handleFailedInitialization(plannerResponse); - } - } - - private void handleSuccessInitialization(AsyncResult> plannerResponse) { - alertHttpService.resetAlertCount(PBS_PLANNER_CLIENT_ERROR); - metrics.updatePlannerRequestMetric(true); - isPlannerResponsive.set(true); - lineItemService.updateIsPlannerResponsive(true); - updateMetaData(plannerResponse.result()); - } - - private void handleFailedInitialization(AsyncResult> plannerResponse) { - final String message = "Failed to retrieve line items from GP. Reason: " + plannerResponse.cause().getMessage(); - alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_PLANNER_CLIENT_ERROR, AlertPriority.MEDIUM, message); - logger.warn(message); - isPlannerResponsive.set(false); - lineItemService.updateIsPlannerResponsive(false); - metrics.updatePlannerRequestMetric(false); - } - - /** - * Overwrites maps with metadata - */ - private void updateMetaData(List metaData) { - lineItemService.updateLineItems(metaData, isPlannerResponsive.get()); - deliveryProgressService.processDeliveryProgressUpdateEvent(); - } -} diff --git a/src/main/java/org/prebid/server/deals/RegisterService.java b/src/main/java/org/prebid/server/deals/RegisterService.java deleted file mode 100644 index fd77aa00248..00000000000 --- a/src/main/java/org/prebid/server/deals/RegisterService.java +++ /dev/null @@ -1,187 +0,0 @@ -package org.prebid.server.deals; - -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.AsyncResult; -import io.vertx.core.MultiMap; -import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.events.AdminEventService; -import org.prebid.server.deals.model.AdminCentralResponse; -import org.prebid.server.deals.model.AlertPriority; -import org.prebid.server.deals.model.DeploymentProperties; -import org.prebid.server.deals.model.PlannerProperties; -import org.prebid.server.deals.proto.CurrencyServiceState; -import org.prebid.server.deals.proto.RegisterRequest; -import org.prebid.server.deals.proto.Status; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.health.HealthMonitor; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.Initializable; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.math.BigDecimal; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.Base64; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -public class RegisterService implements Initializable, Suspendable { - - private static final Logger logger = LoggerFactory.getLogger(RegisterService.class); - - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - - private static final String BASIC_AUTH_PATTERN = "Basic %s"; - private static final String PG_TRX_ID = "pg-trx-id"; - private static final String PBS_REGISTER_CLIENT_ERROR = "pbs-register-client-error"; - private static final String SERVICE_NAME = "register"; - - private final PlannerProperties plannerProperties; - private final DeploymentProperties deploymentProperties; - private final AdminEventService adminEventService; - private final DeliveryProgressService deliveryProgressService; - private final AlertHttpService alertHttpService; - private final HealthMonitor healthMonitor; - private final CurrencyConversionService currencyConversionService; - private final HttpClient httpClient; - private final Vertx vertx; - private final JacksonMapper mapper; - - private final long registerTimeout; - private final long registerPeriod; - private final String basicAuthHeader; - private volatile long registerTimerId; - private volatile boolean isSuspended; - - public RegisterService(PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - AdminEventService adminEventService, - DeliveryProgressService deliveryProgressService, - AlertHttpService alertHttpService, - HealthMonitor healthMonitor, - CurrencyConversionService currencyConversionService, - HttpClient httpClient, - Vertx vertx, - JacksonMapper mapper) { - this.plannerProperties = Objects.requireNonNull(plannerProperties); - this.deploymentProperties = Objects.requireNonNull(deploymentProperties); - this.adminEventService = Objects.requireNonNull(adminEventService); - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.alertHttpService = Objects.requireNonNull(alertHttpService); - this.healthMonitor = Objects.requireNonNull(healthMonitor); - this.currencyConversionService = Objects.requireNonNull(currencyConversionService); - this.httpClient = Objects.requireNonNull(httpClient); - this.vertx = Objects.requireNonNull(vertx); - this.mapper = Objects.requireNonNull(mapper); - - this.registerTimeout = plannerProperties.getTimeoutMs(); - this.registerPeriod = TimeUnit.SECONDS.toMillis(plannerProperties.getRegisterPeriodSeconds()); - this.basicAuthHeader = authHeader(plannerProperties.getUsername(), plannerProperties.getPassword()); - } - - /** - * Creates Authorization header value from username and password. - */ - private static String authHeader(String username, String password) { - return BASIC_AUTH_PATTERN - .formatted(Base64.getEncoder().encodeToString((username + ':' + password).getBytes())); - } - - @Override - public void suspend() { - isSuspended = true; - vertx.cancelTimer(registerTimerId); - } - - @Override - public void initialize() { - registerTimerId = vertx.setPeriodic(registerPeriod, ignored -> performRegistration()); - performRegistration(); - } - - public void performRegistration() { - register(headers()); - } - - protected void register(MultiMap headers) { - if (isSuspended) { - logger.warn("Register request was not sent to general planner, as planner service is suspended from" - + " register endpoint."); - return; - } - - final BigDecimal healthIndex = healthMonitor.calculateHealthIndex(); - final ZonedDateTime currencyLastUpdate = currencyConversionService.getLastUpdated(); - final RegisterRequest request = RegisterRequest.of( - healthIndex, - Status.of(currencyLastUpdate != null - ? CurrencyServiceState.of(UTC_MILLIS_FORMATTER.format(currencyLastUpdate)) - : null, - deliveryProgressService.getOverallDeliveryProgressReport()), - deploymentProperties.getPbsHostId(), - deploymentProperties.getPbsRegion(), - deploymentProperties.getPbsVendor()); - final String body = mapper.encodeToString(request); - - logger.info("Sending register request to Planner, {0} is {1}", PG_TRX_ID, headers.get(PG_TRX_ID)); - logger.debug("Register request payload: {0}", body); - - httpClient.post(plannerProperties.getRegisterEndpoint(), headers, body, registerTimeout) - .onComplete(this::handleRegister); - } - - protected MultiMap headers() { - return MultiMap.caseInsensitiveMultiMap() - .add(HttpUtil.AUTHORIZATION_HEADER, basicAuthHeader) - .add(PG_TRX_ID, UUID.randomUUID().toString()); - } - - private void handleRegister(AsyncResult asyncResult) { - if (asyncResult.failed()) { - final Throwable cause = asyncResult.cause(); - final String errorMessage = "Error occurred while registering with the Planner: " + cause; - alert(errorMessage, logger::warn); - } else { - final HttpClientResponse response = asyncResult.result(); - final int statusCode = response.getStatusCode(); - final String responseBody = response.getBody(); - if (statusCode == HttpResponseStatus.OK.code()) { - if (StringUtils.isNotBlank(responseBody)) { - adminEventService.publishAdminCentralEvent(parseRegisterResponse(responseBody)); - } - alertHttpService.resetAlertCount(PBS_REGISTER_CLIENT_ERROR); - } else { - final String errorMessage = "Planner responded with non-successful code %s, response: %s" - .formatted(statusCode, responseBody); - alert(errorMessage, logger::warn); - } - } - } - - private AdminCentralResponse parseRegisterResponse(String responseBody) { - try { - return mapper.decodeValue(responseBody, AdminCentralResponse.class); - } catch (DecodeException e) { - final String errorMessage = "Cannot parse register response: " + responseBody; - alert(errorMessage, logger::warn); - throw new PreBidException(errorMessage, e); - } - } - - private void alert(String message, Consumer logger) { - alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_REGISTER_CLIENT_ERROR, AlertPriority.MEDIUM, message); - logger.accept(message); - } -} diff --git a/src/main/java/org/prebid/server/deals/Suspendable.java b/src/main/java/org/prebid/server/deals/Suspendable.java deleted file mode 100644 index b834b4badfe..00000000000 --- a/src/main/java/org/prebid/server/deals/Suspendable.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.deals; - -public interface Suspendable { - - void suspend(); -} diff --git a/src/main/java/org/prebid/server/deals/TargetingService.java b/src/main/java/org/prebid/server/deals/TargetingService.java deleted file mode 100644 index 17adefbafe5..00000000000 --- a/src/main/java/org/prebid/server/deals/TargetingService.java +++ /dev/null @@ -1,335 +0,0 @@ -package org.prebid.server.deals; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeType; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.TargetingDefinition; -import org.prebid.server.deals.targeting.interpret.And; -import org.prebid.server.deals.targeting.interpret.DomainMetricAwareExpression; -import org.prebid.server.deals.targeting.interpret.Expression; -import org.prebid.server.deals.targeting.interpret.InIntegers; -import org.prebid.server.deals.targeting.interpret.InStrings; -import org.prebid.server.deals.targeting.interpret.IntersectsIntegers; -import org.prebid.server.deals.targeting.interpret.IntersectsSizes; -import org.prebid.server.deals.targeting.interpret.IntersectsStrings; -import org.prebid.server.deals.targeting.interpret.Matches; -import org.prebid.server.deals.targeting.interpret.Not; -import org.prebid.server.deals.targeting.interpret.Or; -import org.prebid.server.deals.targeting.interpret.Within; -import org.prebid.server.deals.targeting.model.GeoRegion; -import org.prebid.server.deals.targeting.model.Size; -import org.prebid.server.deals.targeting.syntax.BooleanOperator; -import org.prebid.server.deals.targeting.syntax.MatchingFunction; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; -import org.prebid.server.exception.TargetingSyntaxException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.StreamUtil; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * Responsible for parsing and interpreting targeting defined in the Line Items’ metadata - * and determining if individual requests match those targeting conditions. - */ -public class TargetingService { - - private final JacksonMapper mapper; - - public TargetingService(JacksonMapper mapper) { - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Accepts targeting definition expressed in JSON syntax (see below), - * parses it and transforms it into an object supporting efficient evaluation - * of the targeting rules against the OpenRTB2 request. - */ - public TargetingDefinition parseTargetingDefinition(JsonNode targetingDefinition, String lineItemId) { - return TargetingDefinition.of(parseNode(targetingDefinition, lineItemId)); - } - - /** - * Accepts OpenRTB2 request and particular Imp object to evaluate Line Item targeting - * definition against and returns whether it is matched or not. - */ - public boolean matchesTargeting(BidRequest bidRequest, - Imp imp, - TargetingDefinition targetingDefinition, - AuctionContext auctionContext) { - - final RequestContext requestContext = new RequestContext(bidRequest, imp, auctionContext.getTxnLog(), mapper); - return targetingDefinition.getRootExpression().matches(requestContext); - } - - private Expression parseNode(JsonNode node, String lineItemId) { - final Map.Entry field = validateIsSingleElementObject(node); - final String fieldName = field.getKey(); - - if (BooleanOperator.isBooleanOperator(fieldName)) { - return parseBooleanOperator(fieldName, field.getValue(), lineItemId); - } else if (TargetingCategory.isTargetingCategory(fieldName)) { - return parseTargetingCategory(fieldName, field.getValue(), lineItemId); - } else { - throw new TargetingSyntaxException( - "Expected either boolean operator or targeting category, got " + fieldName); - } - } - - private Expression parseBooleanOperator(String fieldName, JsonNode value, String lineItemId) { - final BooleanOperator operator = BooleanOperator.fromString(fieldName); - return switch (operator) { - case AND -> new And(parseArray(value, node -> parseNode(node, lineItemId))); - case OR -> new Or(parseArray(value, node -> parseNode(node, lineItemId))); - case NOT -> new Not(parseNode(value, lineItemId)); - }; - } - - private Expression parseTargetingCategory(String fieldName, JsonNode value, String lineItemId) { - final TargetingCategory category = TargetingCategory.fromString(fieldName); - return switch (category.type()) { - case size -> new IntersectsSizes(category, - parseArrayFunction(value, MatchingFunction.INTERSECTS, this::parseSize)); - case mediaType, userSegment -> new IntersectsStrings(category, - parseArrayFunction(value, MatchingFunction.INTERSECTS, TargetingService::parseString)); - case domain -> prepareDomainExpression(category, value, lineItemId); - case publisherDomain -> new DomainMetricAwareExpression(parseStringFunction(category, value), lineItemId); - case referrer, appBundle, adslot -> parseStringFunction(category, value); - case pagePosition, dow, hour -> new InIntegers(category, - parseArrayFunction(value, MatchingFunction.IN, TargetingService::parseInteger)); - case deviceGeoExt, deviceExt -> new InStrings(category, - parseArrayFunction(value, MatchingFunction.IN, TargetingService::parseString)); - case location -> new Within(category, parseSingleObjectFunction(value, MatchingFunction.WITHIN, - this::parseGeoRegion)); - case bidderParam, userFirstPartyData, siteFirstPartyData -> parseTypedFunction(category, value); - }; - } - - private static Or prepareDomainExpression(TargetingCategory category, JsonNode value, String lineItemId) { - final DomainMetricAwareExpression domainExpression = - new DomainMetricAwareExpression(parseStringFunction(category, value), lineItemId); - - final TargetingCategory publisherDomainCategory = new TargetingCategory(TargetingCategory.Type.publisherDomain); - final DomainMetricAwareExpression publisherDomainExpression = - new DomainMetricAwareExpression(parseStringFunction(publisherDomainCategory, value), lineItemId); - - return new Or(List.of(domainExpression, publisherDomainExpression)); - } - - private static List parseArrayFunction(JsonNode value, MatchingFunction function, - Function mapper) { - - return parseArray(validateIsFunction(value, function), mapper); - } - - private static T parseSingleObjectFunction( - JsonNode value, MatchingFunction function, Function mapper) { - - return mapper.apply(validateIsFunction(value, function)); - } - - private static Expression parseStringFunction(TargetingCategory category, JsonNode value) { - final Map.Entry field = validateIsSingleElementObject(value); - final MatchingFunction function = - validateCompatibleFunction(field, MatchingFunction.MATCHES, MatchingFunction.IN); - - return switch (function) { - case MATCHES -> new Matches(category, parseString(field.getValue())); - case IN -> createInStringsFunction(category, field.getValue()); - default -> throw new IllegalStateException("Unexpected string function " + function.value()); - }; - } - - private static Expression parseTypedFunction(TargetingCategory category, JsonNode value) { - final Map.Entry field = validateIsSingleElementObject(value); - final MatchingFunction function = validateCompatibleFunction(field, - MatchingFunction.MATCHES, MatchingFunction.IN, MatchingFunction.INTERSECTS); - - final JsonNode functionValue = field.getValue(); - return switch (function) { - case MATCHES -> new Matches(category, parseString(functionValue)); - case IN -> parseTypedInFunction(category, functionValue); - case INTERSECTS -> parseTypedIntersectsFunction(category, functionValue); - default -> throw new IllegalStateException("Unexpected typed function " + function.value()); - }; - } - - private Size parseSize(JsonNode node) { - validateIsObject(node); - - final Size size; - try { - size = mapper.mapper().treeToValue(node, Size.class); - } catch (JsonProcessingException e) { - throw new TargetingSyntaxException( - "Exception occurred while parsing size: " + e.getMessage(), e); - } - - if (size.getH() == null || size.getW() == null) { - throw new TargetingSyntaxException("Height and width in size definition could not be null or missing"); - } - - return size; - } - - private static String parseString(JsonNode node) { - validateIsString(node); - - final String value = node.textValue(); - if (StringUtils.isEmpty(value)) { - throw new TargetingSyntaxException("String value could not be empty"); - } - return value; - } - - private static Integer parseInteger(JsonNode node) { - validateIsInteger(node); - - return node.intValue(); - } - - private GeoRegion parseGeoRegion(JsonNode node) { - validateIsObject(node); - - final GeoRegion region; - try { - region = mapper.mapper().treeToValue(node, GeoRegion.class); - } catch (JsonProcessingException e) { - throw new TargetingSyntaxException( - "Exception occurred while parsing geo region: " + e.getMessage(), e); - } - - if (region.getLat() == null || region.getLon() == null || region.getRadiusMiles() == null) { - throw new TargetingSyntaxException( - "Lat, lon and radiusMiles in geo region definition could not be null or missing"); - } - - return region; - } - - private static List parseArray(JsonNode node, Function mapper) { - validateIsArray(node); - - return StreamUtil.asStream(node.spliterator()).map(mapper).toList(); - } - - private static Expression parseTypedInFunction(TargetingCategory category, JsonNode value) { - return parseTypedArrayFunction(category, value, TargetingService::createInIntegersFunction, - TargetingService::createInStringsFunction); - } - - private static Expression parseTypedIntersectsFunction(TargetingCategory category, JsonNode value) { - return parseTypedArrayFunction(category, value, TargetingService::createIntersectsIntegersFunction, - TargetingService::createIntersectsStringsFunction); - } - - private static Expression parseTypedArrayFunction( - TargetingCategory category, JsonNode value, - BiFunction integerCreator, - BiFunction stringCreator) { - - validateIsArray(value); - - final Iterator iterator = value.iterator(); - - final JsonNodeType dataType = iterator.hasNext() ? iterator.next().getNodeType() : JsonNodeType.STRING; - return switch (dataType) { - case NUMBER -> integerCreator.apply(category, value); - case STRING -> stringCreator.apply(category, value); - default -> throw new TargetingSyntaxException("Expected integer or string, got " + dataType); - }; - } - - private static Expression createInIntegersFunction(TargetingCategory category, JsonNode value) { - return new InIntegers(category, parseArray(value, TargetingService::parseInteger)); - } - - private static InStrings createInStringsFunction(TargetingCategory category, JsonNode value) { - return new InStrings(category, parseArray(value, TargetingService::parseString)); - } - - private static Expression createIntersectsStringsFunction(TargetingCategory category, JsonNode value) { - return new IntersectsStrings(category, parseArray(value, TargetingService::parseString)); - } - - private static Expression createIntersectsIntegersFunction(TargetingCategory category, JsonNode value) { - return new IntersectsIntegers(category, parseArray(value, TargetingService::parseInteger)); - } - - private static void validateIsObject(JsonNode value) { - if (!value.isObject()) { - throw new TargetingSyntaxException("Expected object, got " + value.getNodeType()); - } - } - - private static Map.Entry validateIsSingleElementObject(JsonNode value) { - validateIsObject(value); - - if (value.size() != 1) { - throw new TargetingSyntaxException( - "Expected only one element in the object, got " + value.size()); - } - - return value.fields().next(); - } - - private static void validateIsArray(JsonNode value) { - if (!value.isArray()) { - throw new TargetingSyntaxException("Expected array, got " + value.getNodeType()); - } - } - - private static void validateIsString(JsonNode value) { - if (!value.isTextual()) { - throw new TargetingSyntaxException("Expected string, got " + value.getNodeType()); - } - } - - private static void validateIsInteger(JsonNode value) { - if (!value.isInt()) { - throw new TargetingSyntaxException("Expected integer, got " + value.getNodeType()); - } - } - - private static JsonNode validateIsFunction(JsonNode value, MatchingFunction function) { - final Map.Entry field = validateIsSingleElementObject(value); - final String fieldName = field.getKey(); - - if (!MatchingFunction.isMatchingFunction(fieldName)) { - throw new TargetingSyntaxException("Expected matching function, got " + fieldName); - } else if (MatchingFunction.fromString(fieldName) != function) { - throw new TargetingSyntaxException( - "Expected %s matching function, got %s".formatted(function.value(), fieldName)); - } - - return field.getValue(); - } - - private static MatchingFunction validateCompatibleFunction(Map.Entry field, - MatchingFunction... compatibleFunctions) { - final String fieldName = field.getKey(); - - if (!MatchingFunction.isMatchingFunction(fieldName)) { - throw new TargetingSyntaxException("Expected matching function, got " + fieldName); - } - - final MatchingFunction function = MatchingFunction.fromString(fieldName); - if (!Arrays.asList(compatibleFunctions).contains(function)) { - throw new TargetingSyntaxException("Expected one of %s matching functions, got %s".formatted( - Arrays.stream(compatibleFunctions).map(MatchingFunction::value).collect(Collectors.joining(", ")), - fieldName)); - } - return function; - } -} diff --git a/src/main/java/org/prebid/server/deals/UserAdditionalInfoService.java b/src/main/java/org/prebid/server/deals/UserAdditionalInfoService.java deleted file mode 100644 index 58892f3142d..00000000000 --- a/src/main/java/org/prebid/server/deals/UserAdditionalInfoService.java +++ /dev/null @@ -1,319 +0,0 @@ -package org.prebid.server.deals; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Data; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Geo; -import com.iab.openrtb.request.Segment; -import com.iab.openrtb.request.User; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.model.Tuple3; -import org.prebid.server.deals.deviceinfo.DeviceInfoService; -import org.prebid.server.deals.model.DeviceInfo; -import org.prebid.server.deals.model.UserData; -import org.prebid.server.deals.model.UserDetails; -import org.prebid.server.execution.Timeout; -import org.prebid.server.geolocation.GeoLocationService; -import org.prebid.server.geolocation.model.GeoInfo; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.CriteriaLogManager; -import org.prebid.server.proto.openrtb.ext.request.ExtDevice; -import org.prebid.server.proto.openrtb.ext.request.ExtDeviceVendor; -import org.prebid.server.proto.openrtb.ext.request.ExtGeo; -import org.prebid.server.proto.openrtb.ext.request.ExtGeoVendor; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserTime; -import org.prebid.server.util.ObjectUtil; - -import java.time.Clock; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.WeekFields; -import java.util.List; -import java.util.Objects; - -public class UserAdditionalInfoService { - - private static final Logger logger = LoggerFactory.getLogger(UserAdditionalInfoService.class); - - private final LineItemService lineItemService; - private final DeviceInfoService deviceInfoService; - private final GeoLocationService geoLocationService; - private final UserService userService; - private final Clock clock; - private final JacksonMapper mapper; - private final CriteriaLogManager criteriaLogManager; - - public UserAdditionalInfoService(LineItemService lineItemService, - DeviceInfoService deviceInfoService, - GeoLocationService geoLocationService, - UserService userService, - Clock clock, - JacksonMapper mapper, - CriteriaLogManager criteriaLogManager) { - - this.lineItemService = Objects.requireNonNull(lineItemService); - this.deviceInfoService = deviceInfoService; - this.geoLocationService = geoLocationService; - this.userService = Objects.requireNonNull(userService); - this.clock = Objects.requireNonNull(clock); - this.mapper = Objects.requireNonNull(mapper); - this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); - } - - public Future populate(AuctionContext context) { - final boolean accountHasDeals = lineItemService.accountHasDeals(context); - final String accountId = context.getAccount().getId(); - if (!accountHasDeals) { - criteriaLogManager.log( - logger, accountId, "Account %s does not have deals".formatted(accountId), logger::debug); - - return Future.succeededFuture(context); - } - - final Device device = context.getBidRequest().getDevice(); - final Timeout timeout = context.getTimeoutContext().getTimeout(); - final GeoInfo geoInfo = context.getGeoInfo(); - - final CompositeFuture compositeFuture = CompositeFuture.join( - lookupDeviceInfo(device), - geoInfo != null ? Future.succeededFuture(geoInfo) : lookupGeoInfo(device, timeout), - userService.getUserDetails(context, timeout)); - - // AsyncResult has atomic nature: its result() method returns null when at least one future fails. - // So, in handler it is ignored and original CompositeFuture used to process obtained results - // to avoid explicit casting to CompositeFuture implementation. - final Promise> promise = Promise.promise(); - compositeFuture.onComplete(ignored -> handleInfos(compositeFuture, promise, context.getAccount().getId())); - return promise.future().map(tuple -> enrichAuctionContext(context, tuple)); - } - - private Future lookupDeviceInfo(Device device) { - return deviceInfoService != null - ? deviceInfoService.getDeviceInfo(device.getUa()) - : Future.failedFuture("Device info is disabled by configuration"); - } - - private Future lookupGeoInfo(Device device, Timeout timeout) { - return geoLocationService != null - ? geoLocationService.lookup(ObjectUtils.defaultIfNull(device.getIp(), device.getIpv6()), timeout) - : Future.failedFuture("Geo location is disabled by configuration"); - } - - private void handleInfos(CompositeFuture compositeFuture, - Promise> resultPromise, - String account) { - - DeviceInfo deviceInfo = null; - GeoInfo geoInfo = null; - UserDetails userDetails = null; - - for (int i = 0; i < compositeFuture.list().size(); i++) { - final Object o = compositeFuture.resultAt(i); - if (o == null) { - criteriaLogManager.log( - logger, - account, - "Deals processing error: " + compositeFuture.cause(i), - logger::warn); - continue; - } - - if (o instanceof DeviceInfo) { - deviceInfo = (DeviceInfo) o; - } else if (o instanceof GeoInfo) { - geoInfo = (GeoInfo) o; - } else if (o instanceof UserDetails) { - userDetails = (UserDetails) o; - } - } - - resultPromise.complete(Tuple3.of(deviceInfo, geoInfo, userDetails)); - } - - private AuctionContext enrichAuctionContext(AuctionContext auctionContext, - Tuple3 tuple) { - - final DeviceInfo deviceInfo = tuple.getLeft(); - final GeoInfo geoInfo = tuple.getMiddle(); - final UserDetails userDetails = tuple.getRight(); - - final BidRequest bidRequest = auctionContext.getBidRequest(); - final Device originalDevice = bidRequest.getDevice(); - - final BidRequest enrichedBidRequest = bidRequest.toBuilder() - .device(deviceInfo != null || geoInfo != null - ? updateDevice(originalDevice, deviceInfo, geoInfo) - : originalDevice) - .user(updateUser(bidRequest.getUser(), userDetails, geoInfo)) - .build(); - - return auctionContext.toBuilder() - .bidRequest(enrichedBidRequest) - .geoInfo(geoInfo) - .build(); - } - - private Device updateDevice(Device device, DeviceInfo deviceInfo, GeoInfo geoInfo) { - final ExtDevice updatedExtDevice = - fillExtDeviceWith( - fillExtDeviceWith( - ObjectUtil.getIfNotNull(device, Device::getExt), - ObjectUtil.getIfNotNull(deviceInfo, DeviceInfo::getVendor), - extDeviceVendorFrom(deviceInfo)), - ObjectUtil.getIfNotNull(geoInfo, GeoInfo::getVendor), - extDeviceVendorFrom(geoInfo)); - final Geo updatedGeo = updateDeviceGeo(ObjectUtil.getIfNotNull(device, Device::getGeo), geoInfo); - - final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); - return deviceBuilder - .geo(updatedGeo) - .ext(updatedExtDevice) - .build(); - } - - private ExtDevice fillExtDeviceWith(ExtDevice extDevice, String vendor, ExtDeviceVendor extDeviceVendor) { - if (extDeviceVendor.equals(ExtDeviceVendor.EMPTY)) { - return extDevice; - } - - final ExtDevice effectiveExtDevice = extDevice != null ? extDevice : ExtDevice.empty(); - effectiveExtDevice.addProperty(vendor, mapper.mapper().valueToTree(extDeviceVendor)); - - return effectiveExtDevice; - } - - private static ExtDeviceVendor extDeviceVendorFrom(DeviceInfo deviceInfo) { - return deviceInfo != null - ? ExtDeviceVendor.builder() - .type(deviceInfo.getDeviceTypeRaw()) - .osfamily(null) - .os(deviceInfo.getOs()) - .osver(deviceInfo.getOsVersion()) - .browser(deviceInfo.getBrowser()) - .browserver(deviceInfo.getBrowserVersion()) - .make(deviceInfo.getManufacturer()) - .model(deviceInfo.getModel()) - .language(deviceInfo.getLanguage()) - .carrier(deviceInfo.getCarrier()) - .build() - : ExtDeviceVendor.EMPTY; - } - - private static ExtDeviceVendor extDeviceVendorFrom(GeoInfo geoInfo) { - return geoInfo != null - ? ExtDeviceVendor.builder() - .connspeed(geoInfo.getConnectionSpeed()) - .build() - : ExtDeviceVendor.EMPTY; - } - - private Geo updateDeviceGeo(Geo geo, GeoInfo geoInfo) { - if (geoInfo == null) { - return geo; - } - - final ExtGeo updatedExtGeo = fillExtGeoWith( - ObjectUtil.getIfNotNull(geo, Geo::getExt), - geoInfo.getVendor(), - extGeoVendorFrom(geoInfo)); - - final Geo.GeoBuilder geoBuilder = geo != null ? geo.toBuilder() : Geo.builder(); - return geoBuilder - .country(geoInfo.getCountry()) - .region(geoInfo.getRegion()) - .metro(geoInfo.getMetroGoogle()) - .lat(geoInfo.getLat()) - .lon(geoInfo.getLon()) - .ext(updatedExtGeo) - .build(); - } - - private ExtGeo fillExtGeoWith(ExtGeo extGeo, String vendor, ExtGeoVendor extGeoVendor) { - if (extGeoVendor.equals(ExtGeoVendor.EMPTY)) { - return extGeo; - } - - final ExtGeo effectiveExtGeo = extGeo != null ? extGeo : ExtGeo.of(); - effectiveExtGeo.addProperty(vendor, mapper.mapper().valueToTree(extGeoVendor)); - - return effectiveExtGeo; - } - - private static ExtGeoVendor extGeoVendorFrom(GeoInfo geoInfo) { - return ExtGeoVendor.builder() - .continent(geoInfo.getContinent()) - .country(geoInfo.getCountry()) - .region(geoInfo.getRegionCode()) - .metro(geoInfo.getMetroNielsen()) - .city(geoInfo.getCity()) - .zip(geoInfo.getZip()) - .build(); - } - - private User updateUser(User user, UserDetails userDetails, GeoInfo geoInfo) { - final User.UserBuilder userBuilder = user != null ? user.toBuilder() : User.builder(); - return userBuilder - .data(userDetails != null ? makeData(userDetails) : null) - .ext(updateExtUser(ObjectUtil.getIfNotNull(user, User::getExt), userDetails, geoInfo)) - .build(); - } - - private static List makeData(UserDetails userDetails) { - final List userData = userDetails.getUserData(); - return userData != null - ? userData.stream() - .map(userDataElement -> Data.builder() - .id(userDataElement.getName()) - .segment(makeSegments(userDataElement.getSegment())) - .build()) - .toList() - : null; - } - - private static List makeSegments(List segments) { - return segments != null - ? segments.stream() - .map(segment -> Segment.builder().id(segment.getId()).build()) - .toList() - : null; - } - - private ExtUser updateExtUser(ExtUser extUser, UserDetails userDetails, GeoInfo geoInfo) { - final ExtUser.ExtUserBuilder extUserBuilder = extUser != null ? extUser.toBuilder() : ExtUser.builder(); - return extUserBuilder - .fcapIds(ObjectUtils.defaultIfNull( - resolveFcapIds(userDetails), - ObjectUtil.getIfNotNull(extUser, ExtUser::getFcapIds))) - .time(resolveExtUserTime(geoInfo)) - .build(); - } - - private static List resolveFcapIds(UserDetails userDetails) { - return userDetails != null - // Indicate that the call to User Data Store has been made successfully even if the user is not frequency - // capped - ? ListUtils.emptyIfNull(userDetails.getFcapIds()) - // otherwise leave cappedIds null to indicate that call to User Data Store failed - : null; - } - - private ExtUserTime resolveExtUserTime(GeoInfo geoInfo) { - final ZoneId timeZone = ObjectUtils.firstNonNull( - ObjectUtil.getIfNotNull(geoInfo, GeoInfo::getTimeZone), - clock.getZone()); - - final ZonedDateTime dateTime = ZonedDateTime.now(clock).withZoneSameInstant(timeZone); - - return ExtUserTime.of( - dateTime.getDayOfWeek().get(WeekFields.SUNDAY_START.dayOfWeek()), - dateTime.getHour()); - } -} diff --git a/src/main/java/org/prebid/server/deals/UserService.java b/src/main/java/org/prebid/server/deals/UserService.java deleted file mode 100644 index 41e0b300b67..00000000000 --- a/src/main/java/org/prebid/server/deals/UserService.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.prebid.server.deals; - -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.cache.model.DebugHttpCall; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.cookie.model.UidWithExpiry; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.deals.model.User; -import org.prebid.server.deals.model.UserDetails; -import org.prebid.server.deals.model.UserDetailsProperties; -import org.prebid.server.deals.model.UserDetailsRequest; -import org.prebid.server.deals.model.UserDetailsResponse; -import org.prebid.server.deals.model.UserId; -import org.prebid.server.deals.model.UserIdRule; -import org.prebid.server.deals.model.WinEventNotification; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.handler.NotificationEventHandler; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.metric.Metrics; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Works with user related information. - */ -public class UserService { - - private static final Logger logger = LoggerFactory.getLogger(UserService.class); - private static final String USER_SERVICE = "userservice"; - - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - - private final LineItemService lineItemService; - private final HttpClient httpClient; - private final Clock clock; - private final Metrics metrics; - private final JacksonMapper mapper; - - private final String userDetailsUrl; - private final String winEventUrl; - private final long timeout; - private final List userIdRules; - private final String dataCenterRegion; - - public UserService(UserDetailsProperties userDetailsProperties, - String dataCenterRegion, - LineItemService lineItemService, - HttpClient httpClient, - Clock clock, - Metrics metrics, - JacksonMapper mapper) { - - this.lineItemService = Objects.requireNonNull(lineItemService); - this.httpClient = Objects.requireNonNull(httpClient); - this.clock = Objects.requireNonNull(clock); - this.metrics = Objects.requireNonNull(metrics); - - this.userDetailsUrl = Objects.requireNonNull( - HttpUtil.validateUrl(userDetailsProperties.getUserDetailsEndpoint())); - this.winEventUrl = Objects.requireNonNull(HttpUtil.validateUrl(userDetailsProperties.getWinEventEndpoint())); - this.timeout = userDetailsProperties.getTimeout(); - this.userIdRules = Objects.requireNonNull(userDetailsProperties.getUserIds()); - this.dataCenterRegion = Objects.requireNonNull(dataCenterRegion); - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Fetches {@link UserDetails} from the User Data Store. - */ - public Future getUserDetails(AuctionContext context, Timeout timeout) { - final Map uidsMap = context.getUidsCookie().getCookieUids().getUids(); - if (CollectionUtils.isEmpty(uidsMap.values())) { - metrics.updateUserDetailsRequestPreparationFailed(); - context.getDebugHttpCalls().put(USER_SERVICE, Collections.singletonList(DebugHttpCall.empty())); - return Future.succeededFuture(UserDetails.empty()); - } - - final List userIds = getUserIds(uidsMap); - if (CollectionUtils.isEmpty(userIds)) { - metrics.updateUserDetailsRequestPreparationFailed(); - context.getDebugHttpCalls().put(USER_SERVICE, Collections.singletonList(DebugHttpCall.empty())); - return Future.succeededFuture(UserDetails.empty()); - } - - final UserDetailsRequest userDetailsRequest = UserDetailsRequest.of( - UTC_MILLIS_FORMATTER.format(ZonedDateTime.now(clock)), userIds); - final String body = mapper.encodeToString(userDetailsRequest); - - final long requestTimeout = Math.min(this.timeout, timeout.remaining()); - - final long startTime = clock.millis(); - return httpClient.post(userDetailsUrl, body, requestTimeout) - .map(httpClientResponse -> toUserServiceResult(httpClientResponse, context, - userDetailsUrl, body, startTime)) - .recover(throwable -> failGetDetailsResponse(throwable, context, userDetailsUrl, body, startTime)); - } - - /** - * Retrieves the UID from UIDs Map by each {@link UserIdRule#getLocation()} and if UID is present - creates a - * {@link UserId} object that contains {@link UserIdRule#getType()} and UID and adds it to UserId list. - */ - private List getUserIds(Map bidderToUid) { - final List userIds = new ArrayList<>(); - for (UserIdRule rule : userIdRules) { - final UidWithExpiry uid = bidderToUid.get(rule.getLocation()); - if (uid != null) { - userIds.add(UserId.of(rule.getType(), uid.getUid())); - } - } - return userIds; - } - - /** - * Transforms response from User Data Store into {@link Future} of {@link UserDetails}. - *

- * Throws {@link PreBidException} if an error occurs during response body deserialization. - */ - private UserDetails toUserServiceResult(HttpClientResponse clientResponse, AuctionContext context, - String requestUrl, String requestBody, long startTime) { - final int responseStatusCode = clientResponse.getStatusCode(); - verifyStatusCode(responseStatusCode); - - final String responseBody = clientResponse.getBody(); - final User user; - final int responseTime = responseTime(startTime); - try { - user = parseUserDetailsResponse(responseBody); - } finally { - context.getDebugHttpCalls().put(USER_SERVICE, Collections.singletonList( - DebugHttpCall.builder() - .requestUri(requestUrl) - .requestBody(requestBody) - .responseStatus(responseStatusCode) - .responseBody(responseBody) - .responseTimeMillis(responseTime) - .build())); - } - metrics.updateRequestTimeMetric(MetricName.user_details_request_time, responseTime); - metrics.updateUserDetailsRequestMetric(true); - return UserDetails.of(user.getData(), user.getExt().getFcapIds()); - } - - private User parseUserDetailsResponse(String responseBody) { - final UserDetailsResponse userDetailsResponse; - try { - userDetailsResponse = mapper.decodeValue(responseBody, UserDetailsResponse.class); - } catch (DecodeException e) { - throw new PreBidException("Cannot parse response: " + responseBody, e); - } - - final User user = userDetailsResponse.getUser(); - if (user == null) { - throw new PreBidException("Field 'user' is missing in response: " + responseBody); - } - - if (user.getData() == null) { - throw new PreBidException("Field 'user.data' is missing in response: " + responseBody); - } - - if (user.getExt() == null) { - throw new PreBidException("Field 'user.ext' is missing in response: " + responseBody); - } - return user; - } - - /** - * Throw {@link PreBidException} if response status is not 200. - */ - private static void verifyStatusCode(int statusCode) { - if (statusCode != 200) { - throw new PreBidException("Bad response status code: " + statusCode); - } - } - - /** - * Handles errors that occurred during getUserDetails HTTP request or response processing. - */ - private Future failGetDetailsResponse(Throwable exception, AuctionContext context, String requestUrl, - String requestBody, long startTime) { - final int responseTime = responseTime(startTime); - context.getDebugHttpCalls().putIfAbsent(USER_SERVICE, - Collections.singletonList( - DebugHttpCall.builder() - .requestUri(requestUrl) - .requestBody(requestBody) - .responseTimeMillis(responseTime) - .build())); - metrics.updateUserDetailsRequestMetric(false); - metrics.updateRequestTimeMetric(MetricName.user_details_request_time, responseTime); - logger.warn("Error occurred while fetching user details", exception); - return Future.failedFuture(exception); - } - - /** - * Calculates execution time since the given start time. - */ - private int responseTime(long startTime) { - return Math.toIntExact(clock.millis() - startTime); - } - - /** - * Accepts lineItemId and bidId from the {@link NotificationEventHandler}, - * joins event data with corresponding Line Item metadata (provided by LineItemService) - * and passes this information to the User Data Store to facilitate frequency capping. - */ - public void processWinEvent(String lineItemId, String bidId, UidsCookie uids) { - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - final List userIds = getUserIds(uids.getCookieUids().getUids()); - - if (!hasRequiredData(lineItem, userIds, lineItemId)) { - metrics.updateWinRequestPreparationFailed(); - return; - } - - final String body = mapper.encodeToString(WinEventNotification.builder() - .bidderCode(lineItem.getSource()) - .bidId(bidId) - .lineItemId(lineItemId) - .region(dataCenterRegion) - .userIds(userIds) - .winEventDateTime(ZonedDateTime.now(clock)) - .lineUpdatedDateTime(lineItem.getUpdatedTimeStamp()) - .frequencyCaps(lineItem.getFrequencyCaps()) - .build()); - - metrics.updateWinNotificationMetric(); - final long startTime = clock.millis(); - httpClient.post(winEventUrl, body, timeout) - .onComplete(result -> handleWinResponse(result, startTime)); - } - - /** - * Verify that all necessary data is present and log error if something is missing. - */ - private static boolean hasRequiredData(LineItem lineItem, List userIds, String lineItemId) { - if (lineItem == null) { - logger.error("Meta Data for Line Item Id {0} does not exist", lineItemId); - return false; - } - - if (CollectionUtils.isEmpty(userIds)) { - logger.error("User Ids cannot be empty"); - return false; - } - return true; - } - - /** - * Checks response from User Data Store. - */ - private void handleWinResponse(AsyncResult asyncResult, long startTime) { - metrics.updateWinRequestTime(responseTime(startTime)); - if (asyncResult.succeeded()) { - try { - verifyStatusCode(asyncResult.result().getStatusCode()); - metrics.updateWinEventRequestMetric(true); - } catch (PreBidException e) { - metrics.updateWinEventRequestMetric(false); - logWinEventError(e); - } - } else { - metrics.updateWinEventRequestMetric(false); - logWinEventError(asyncResult.cause()); - } - } - - /** - * Logs errors that occurred during processWinEvent HTTP request or bad response code. - */ - private static void logWinEventError(Throwable exception) { - logger.warn("Error occurred while pushing win event notification", exception); - } -} diff --git a/src/main/java/org/prebid/server/deals/deviceinfo/DeviceInfoService.java b/src/main/java/org/prebid/server/deals/deviceinfo/DeviceInfoService.java deleted file mode 100644 index 1e680ebd51f..00000000000 --- a/src/main/java/org/prebid/server/deals/deviceinfo/DeviceInfoService.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.deals.deviceinfo; - -import io.vertx.core.Future; -import org.prebid.server.deals.model.DeviceInfo; - -/** - * Processes device related information. - */ -@FunctionalInterface -public interface DeviceInfoService { - - /** - * Provides information about device based on User-Agent string and other available attributes. - */ - Future getDeviceInfo(String ua); -} diff --git a/src/main/java/org/prebid/server/deals/events/AdminEventProcessor.java b/src/main/java/org/prebid/server/deals/events/AdminEventProcessor.java deleted file mode 100644 index dbf9133a902..00000000000 --- a/src/main/java/org/prebid/server/deals/events/AdminEventProcessor.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.prebid.server.deals.events; - -import org.prebid.server.deals.model.AdminCentralResponse; - -public interface AdminEventProcessor { - - void processAdminCentralEvent(AdminCentralResponse adminCentralResponse); -} diff --git a/src/main/java/org/prebid/server/deals/events/AdminEventService.java b/src/main/java/org/prebid/server/deals/events/AdminEventService.java deleted file mode 100644 index f8f6130c6b5..00000000000 --- a/src/main/java/org/prebid/server/deals/events/AdminEventService.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.deals.events; - -import io.vertx.core.eventbus.DeliveryOptions; -import io.vertx.core.eventbus.EventBus; -import org.prebid.server.deals.model.AdminCentralResponse; -import org.prebid.server.vertx.LocalMessageCodec; - -import java.util.Objects; - -public class AdminEventService { - - private static final String ADDRESS_ADMIN_CENTRAL_COMMAND = "event.admin-central"; - - private static final DeliveryOptions DELIVERY_OPTIONS = - new DeliveryOptions() - .setCodecName(LocalMessageCodec.codecName()); - - private final EventBus eventBus; - - public AdminEventService(EventBus eventBus) { - this.eventBus = Objects.requireNonNull(eventBus); - } - - /** - * Publishes admin central event. - */ - public void publishAdminCentralEvent(AdminCentralResponse adminCentralResponse) { - eventBus.publish(ADDRESS_ADMIN_CENTRAL_COMMAND, adminCentralResponse, DELIVERY_OPTIONS); - } -} diff --git a/src/main/java/org/prebid/server/deals/events/ApplicationEventProcessor.java b/src/main/java/org/prebid/server/deals/events/ApplicationEventProcessor.java deleted file mode 100644 index ed1595d1eda..00000000000 --- a/src/main/java/org/prebid/server/deals/events/ApplicationEventProcessor.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.deals.events; - -import org.prebid.server.auction.model.AuctionContext; - -/** - * Interface for the components able to consume application events. - * - * @see ApplicationEventService - */ -public interface ApplicationEventProcessor { - - void processAuctionEvent(AuctionContext auctionContext); - - void processLineItemWinEvent(String lineItemId); - - void processDeliveryProgressUpdateEvent(); -} diff --git a/src/main/java/org/prebid/server/deals/events/ApplicationEventService.java b/src/main/java/org/prebid/server/deals/events/ApplicationEventService.java deleted file mode 100644 index 78dec0ae622..00000000000 --- a/src/main/java/org/prebid/server/deals/events/ApplicationEventService.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.prebid.server.deals.events; - -import io.vertx.core.eventbus.DeliveryOptions; -import io.vertx.core.eventbus.EventBus; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.vertx.LocalMessageCodec; - -import java.util.Objects; - -/** - * Main purpose of this service is decoupling of application events delivery from their generators to consumers. - *

- * This service is essentially a facade for Vert.x {@link EventBus}, it encapsulates addressing and consumers - * configuration concerns and provides type-safe API for publishing different application events which are consumed - * by all {@link ApplicationEventProcessor}s registered in the application. - *

- * Implementation notes: - * Communication through {@link EventBus} is performed only locally, that's why no serialization/deserialization - * happens for objects passed over the bus and hence no implied performance penalty (see {@link LocalMessageCodec}). - */ -public class ApplicationEventService { - - private static final String ADDRESS_EVENT_OPENRTB2_AUCTION = "event.openrtb2-auction"; - private static final String ADDRESS_EVENT_LINE_ITEM_WIN = "event.line-item-win"; - private static final String ADDRESS_EVENT_DELIVERY_UPDATE = "event.delivery-update"; - - private static final DeliveryOptions DELIVERY_OPTIONS = - new DeliveryOptions() - .setCodecName(LocalMessageCodec.codecName()); - - private final EventBus eventBus; - - public ApplicationEventService(EventBus eventBus) { - this.eventBus = Objects.requireNonNull(eventBus); - } - - /** - * Publishes auction event. - */ - public void publishAuctionEvent(AuctionContext auctionContext) { - eventBus.publish(ADDRESS_EVENT_OPENRTB2_AUCTION, auctionContext, DELIVERY_OPTIONS); - } - - /** - * Publishes line item win event. - */ - public void publishLineItemWinEvent(String lineItemId) { - eventBus.publish(ADDRESS_EVENT_LINE_ITEM_WIN, lineItemId); - } - - /** - * Publishes delivery update event. - */ - public void publishDeliveryUpdateEvent() { - eventBus.publish(ADDRESS_EVENT_DELIVERY_UPDATE, null); - } -} diff --git a/src/main/java/org/prebid/server/deals/events/EventServiceInitializer.java b/src/main/java/org/prebid/server/deals/events/EventServiceInitializer.java deleted file mode 100644 index 93802bbadd0..00000000000 --- a/src/main/java/org/prebid/server/deals/events/EventServiceInitializer.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.prebid.server.deals.events; - -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.eventbus.Message; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.deals.model.AdminCentralResponse; -import org.prebid.server.vertx.Initializable; - -import java.util.List; -import java.util.Objects; - -public class EventServiceInitializer implements Initializable { - - private static final String ADDRESS_EVENT_OPENRTB2_AUCTION = "event.openrtb2-auction"; - private static final String ADDRESS_EVENT_LINE_ITEM_WIN = "event.line-item-win"; - private static final String ADDRESS_EVENT_DELIVERY_UPDATE = "event.delivery-update"; - private static final String ADDRESS_ADMIN_CENTRAL_COMMAND = "event.admin-central"; - - private final List applicationEventProcessors; - private final List adminEventProcessors; - private final EventBus eventBus; - - public EventServiceInitializer(List applicationEventProcessors, - List adminEventProcessors, - EventBus eventBus) { - this.applicationEventProcessors = Objects.requireNonNull(applicationEventProcessors); - this.adminEventProcessors = Objects.requireNonNull(adminEventProcessors); - this.eventBus = Objects.requireNonNull(eventBus); - } - - @Override - public void initialize() { - eventBus.localConsumer( - ADDRESS_EVENT_OPENRTB2_AUCTION, - (Message message) -> applicationEventProcessors.forEach( - recorder -> recorder.processAuctionEvent(message.body()))); - - eventBus.localConsumer( - ADDRESS_EVENT_LINE_ITEM_WIN, - (Message message) -> applicationEventProcessors.forEach( - recorder -> recorder.processLineItemWinEvent(message.body()))); - - eventBus.localConsumer( - ADDRESS_EVENT_DELIVERY_UPDATE, - (Message message) -> applicationEventProcessors.forEach( - ApplicationEventProcessor::processDeliveryProgressUpdateEvent)); - - eventBus.localConsumer( - ADDRESS_ADMIN_CENTRAL_COMMAND, - (Message message) -> adminEventProcessors.forEach( - recorder -> recorder.processAdminCentralEvent(message.body()))); - } -} diff --git a/src/main/java/org/prebid/server/deals/lineitem/DeliveryPlan.java b/src/main/java/org/prebid/server/deals/lineitem/DeliveryPlan.java deleted file mode 100644 index fe373f114f5..00000000000 --- a/src/main/java/org/prebid/server/deals/lineitem/DeliveryPlan.java +++ /dev/null @@ -1,184 +0,0 @@ -package org.prebid.server.deals.lineitem; - -import org.apache.commons.collections4.SetUtils; -import org.prebid.server.deals.proto.DeliverySchedule; -import org.prebid.server.deals.proto.Token; - -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class DeliveryPlan { - - private final DeliverySchedule deliverySchedule; - - private final Set deliveryTokens; - - private DeliveryPlan(DeliverySchedule deliverySchedule) { - this(Objects.requireNonNull(deliverySchedule), toDeliveryTokens(deliverySchedule.getTokens())); - } - - private DeliveryPlan(DeliverySchedule deliverySchedule, Set deliveryTokens) { - this.deliverySchedule = Objects.requireNonNull(deliverySchedule); - this.deliveryTokens = Objects.requireNonNull(deliveryTokens); - } - - public static DeliveryPlan of(DeliverySchedule deliverySchedule) { - return new DeliveryPlan(deliverySchedule); - } - - /** - * Returns number of not spent tokens in {@link DeliveryPlan}. - */ - public int getUnspentTokens() { - return deliveryTokens.stream().mapToInt(DeliveryToken::getUnspent).sum(); - } - - /** - * Returns number of spent tokens in {@link DeliveryPlan}. - */ - public long getSpentTokens() { - return deliveryTokens.stream().map(DeliveryToken::getSpent).mapToLong(LongAdder::sum).sum(); - } - - public long getTotalTokens() { - return deliveryTokens.stream().mapToLong(DeliveryToken::getTotal).sum(); - } - - /** - * Returns lowest (which means highest priority) token's class value with unspent tokens. - */ - public Integer getHighestUnspentTokensClass() { - return deliveryTokens.stream() - .filter(token -> token.getUnspent() > 0) - .map(DeliveryToken::getPriorityClass) - .findFirst() - .orElse(null); - } - - /** - * Increments tokens in {@link DeliveryToken} with highest priority within {@link DeliveryPlan} - * - * @return class of the token incremented - */ - public Integer incSpentToken() { - final DeliveryToken unspentToken = deliveryTokens.stream() - .filter(token -> token.getUnspent() > 0) - .findFirst() - .orElse(null); - if (unspentToken != null) { - unspentToken.inc(); - return unspentToken.getPriorityClass(); - } - - return null; - } - - /** - * Merges tokens from expired {@link DeliveryPlan} to the next one. - */ - public DeliveryPlan mergeWithNextDeliverySchedule(DeliverySchedule nextDeliverySchedule, boolean sumTotal) { - - final Map nextTokensByClass = nextDeliverySchedule.getTokens().stream() - .collect(Collectors.toMap(Token::getPriorityClass, Function.identity())); - - final Set mergedTokens = new TreeSet<>(); - - for (final DeliveryToken expiredToken : deliveryTokens) { - final Integer priorityClass = expiredToken.getPriorityClass(); - final Token nextToken = nextTokensByClass.get(priorityClass); - - mergedTokens.add(expiredToken.mergeWithToken(nextToken, sumTotal)); - - nextTokensByClass.remove(priorityClass); - } - - // add remaining (not merged) tokens - nextTokensByClass.values().stream().map(DeliveryToken::of).forEach(mergedTokens::add); - - return new DeliveryPlan(nextDeliverySchedule, mergedTokens); - } - - public DeliveryPlan mergeWithNextDeliveryPlan(DeliveryPlan anotherPlan) { - return mergeWithNextDeliverySchedule(anotherPlan.deliverySchedule, false); - } - - public DeliveryPlan withoutSpentTokens() { - return new DeliveryPlan(deliverySchedule, deliveryTokens.stream() - .map(DeliveryToken::of) - .collect(Collectors.toSet())); - } - - public void incTokenWithPriority(Integer tokenPriority) { - deliveryTokens.stream() - .filter(token -> Objects.equals(token.getPriorityClass(), tokenPriority)) - .findAny() - .ifPresent(DeliveryToken::inc); - } - - /** - * Calculates readyAt from expirationDate and number of unspent tokens. - */ - public ZonedDateTime calculateReadyAt() { - final ZonedDateTime planStartTime = deliverySchedule.getStartTimeStamp(); - final long spentTokens = getSpentTokens(); - final long unspentTokens = getUnspentTokens(); - final long timeShift = spentTokens * ((deliverySchedule.getEndTimeStamp().toInstant().toEpochMilli() - - planStartTime.toInstant().toEpochMilli()) / getTotalTokens()); - return unspentTokens > 0 - ? ZonedDateTime.ofInstant(planStartTime.toInstant().plusMillis(timeShift), ZoneOffset.UTC) - : null; - } - - public Long getDeliveryRateInMilliseconds() { - return getUnspentTokens() > 0 - ? (deliverySchedule.getEndTimeStamp().toInstant().toEpochMilli() - - deliverySchedule.getStartTimeStamp().toInstant().toEpochMilli()) - / getTotalTokens() - : null; - } - - public boolean isUpdated(DeliverySchedule deliverySchedule) { - final ZonedDateTime currentPlanUpdatedDate = this.deliverySchedule.getUpdatedTimeStamp(); - final ZonedDateTime newPlanUpdatedDate = deliverySchedule.getUpdatedTimeStamp(); - return !(currentPlanUpdatedDate == null && newPlanUpdatedDate == null) - && (currentPlanUpdatedDate == null || newPlanUpdatedDate == null - || currentPlanUpdatedDate.isBefore(newPlanUpdatedDate)); - } - - public String getPlanId() { - return deliverySchedule.getPlanId(); - } - - public ZonedDateTime getStartTimeStamp() { - return deliverySchedule.getStartTimeStamp(); - } - - public ZonedDateTime getEndTimeStamp() { - return deliverySchedule.getEndTimeStamp(); - } - - public ZonedDateTime getUpdatedTimeStamp() { - return deliverySchedule.getUpdatedTimeStamp(); - } - - public Set getDeliveryTokens() { - return deliveryTokens; - } - - public DeliverySchedule getDeliverySchedule() { - return deliverySchedule; - } - - private static Set toDeliveryTokens(Set tokens) { - return SetUtils.emptyIfNull(tokens).stream() - .map(DeliveryToken::of) - .collect(Collectors.toCollection(TreeSet::new)); - } -} diff --git a/src/main/java/org/prebid/server/deals/lineitem/DeliveryProgress.java b/src/main/java/org/prebid/server/deals/lineitem/DeliveryProgress.java deleted file mode 100644 index f14dbef4a07..00000000000 --- a/src/main/java/org/prebid/server/deals/lineitem/DeliveryProgress.java +++ /dev/null @@ -1,349 +0,0 @@ -package org.prebid.server.deals.lineitem; - -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.model.TxnLog; -import org.prebid.server.deals.proto.report.Event; - -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.Collection; -import java.util.Comparator; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -public class DeliveryProgress { - - private static final String WIN_EVENT_TYPE = "win"; - - private final Map lineItemStatuses; - private final Map requestsPerAccount; - private final Map> lineItemIdToLost; - private final LongAdder requests; - private ZonedDateTime startTimeStamp; - private ZonedDateTime endTimeStamp; - private final LineItemService lineItemService; - - private DeliveryProgress(ZonedDateTime startTimeStamp, LineItemService lineItemService) { - this.startTimeStamp = Objects.requireNonNull(startTimeStamp); - this.lineItemStatuses = new ConcurrentHashMap<>(); - this.requests = new LongAdder(); - this.requestsPerAccount = new ConcurrentHashMap<>(); - this.lineItemIdToLost = new ConcurrentHashMap<>(); - this.lineItemService = Objects.requireNonNull(lineItemService); - } - - public static DeliveryProgress of(ZonedDateTime startTimeStamp, LineItemService lineItemService) { - return new DeliveryProgress(startTimeStamp, lineItemService); - } - - public DeliveryProgress copyWithOriginalPlans() { - final DeliveryProgress progress = DeliveryProgress.of(this.getStartTimeStamp(), - this.lineItemService); - - for (final LineItemStatus originalStatus : this.lineItemStatuses.values()) { - progress.lineItemStatuses.put(originalStatus.getLineItemId(), createStatusWithPlans(originalStatus)); - } - - progress.mergeFrom(this); - - return progress; - } - - private LineItemStatus createStatusWithPlans(LineItemStatus originalStatus) { - final LineItemStatus status = createLineItemStatus(originalStatus.getLineItemId()); - status.getDeliveryPlans().addAll(originalStatus.getDeliveryPlans()); - return status; - } - - /** - * Updates delivery progress from {@link TxnLog}. - */ - public void recordTransactionLog(TxnLog txnLog, Map planIdToTokenPriority, String accountId) { - accountRequests(accountId).increment(); - requests.increment(); - - txnLog.lineItemSentToClientAsTopMatch() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToClientAsTopMatch)); - txnLog.lineItemsSentToClient() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToClient)); - txnLog.lineItemsMatchedDomainTargeting() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incDomainMatched)); - txnLog.lineItemsMatchedWholeTargeting() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incTargetMatched)); - txnLog.lineItemsMatchedTargetingFcapped() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incTargetMatchedButFcapped)); - txnLog.lineItemsMatchedTargetingFcapLookupFailed() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incTargetMatchedButFcapLookupFailed)); - txnLog.lineItemsPacingDeferred() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incPacingDeferred)); - txnLog.lineItemsSentToBidder().values().forEach(idList -> idList - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToBidder))); - txnLog.lineItemsSentToBidderAsTopMatch().values().forEach(bidderList -> bidderList - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToBidderAsTopMatch))); - txnLog.lineItemsReceivedFromBidder().values().forEach(idList -> idList - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incReceivedFromBidder))); - txnLog.lineItemsResponseInvalidated() - .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incReceivedFromBidderInvalidated)); - - txnLog.lineItemSentToClientAsTopMatch() - .forEach(lineItemId -> incToken(lineItemId, planIdToTokenPriority)); - - txnLog.lostMatchingToLineItems().forEach((lineItemId, lostToLineItemsIds) -> - updateLostToEachLineItem(lineItemId, lostToLineItemsIds, lineItemIdToLost)); - txnLog.lostAuctionToLineItems().forEach((lineItemId, lostToLineItemsIds) -> - updateLostToEachLineItem(lineItemId, lostToLineItemsIds, lineItemIdToLost)); - } - - /** - * Increments {@link LineItemStatus} win type {@link Event} counter. Creates new {@link LineItemStatus} if not - * exists. - */ - public void recordWinEvent(String lineItemId) { - final LineItemStatus lineItemStatus = lineItemStatuses.computeIfAbsent(lineItemId, this::createLineItemStatus); - final Event winEvent = lineItemStatus.getEvents().stream() - .filter(event -> event.getType().equals(WIN_EVENT_TYPE)) - .findAny() - .orElseGet(() -> Event.of(WIN_EVENT_TYPE, new LongAdder())); - - winEvent.getCount().increment(); - lineItemStatus.getEvents().add(winEvent); - } - - private LineItemStatus createLineItemStatus(String lineItemId) { - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - return lineItem != null - ? LineItemStatus.of(lineItem) - : LineItemStatus.of(lineItemId); - } - - /** - * Updates delivery progress from another {@link DeliveryProgress}. - */ - public void mergeFrom(DeliveryProgress another) { - requests.add(another.requests.sum()); - - another.requestsPerAccount.forEach((accountId, requestsCount) -> - mergeRequestsCount(accountId, requestsCount, requestsPerAccount)); - - another.lineItemStatuses.forEach((lineItemId, lineItemStatus) -> - lineItemStatuses.computeIfAbsent(lineItemId, this::createLineItemStatus).merge(lineItemStatus)); - - another.lineItemIdToLost.forEach((lineItemId, currentLineItemLost) -> - mergeCurrentLineItemLostReportToOverall(lineItemId, currentLineItemLost, lineItemIdToLost)); - } - - public void upsertPlanReferenceFromLineItem(LineItem lineItem) { - final String lineItemId = lineItem.getLineItemId(); - final LineItemStatus existingLineItemStatus = lineItemStatuses.get(lineItemId); - final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); - if (existingLineItemStatus == null) { - final LineItemStatus lineItemStatus = createLineItemStatus(lineItem.getLineItemId()); - lineItemStatus.getDeliveryPlans().add(activeDeliveryPlan); - lineItemStatuses.put(lineItemId, lineItemStatus); - } else { - updateLineItemStatusWithActiveDeliveryPlan(existingLineItemStatus, activeDeliveryPlan); - } - } - - /** - * Updates {@link LineItemStatus} with current {@link DeliveryPlan}. - */ - public void mergePlanFromLineItem(LineItem lineItem) { - final LineItemStatus currentLineItemStatus = lineItemStatuses.computeIfAbsent(lineItem.getLineItemId(), - this::createLineItemStatus); - final DeliveryPlan updatedDeliveryPlan = lineItem.getActiveDeliveryPlan(); - - final Set deliveryPlans = currentLineItemStatus.getDeliveryPlans(); - final DeliveryPlan currentPlan = deliveryPlans.stream() - .filter(plan -> Objects.equals(plan.getPlanId(), updatedDeliveryPlan.getPlanId())) - .findFirst() - .orElse(null); - - if (currentPlan == null) { - deliveryPlans.add(updatedDeliveryPlan.withoutSpentTokens()); - } else if (currentPlan.isUpdated(updatedDeliveryPlan.getDeliverySchedule())) { - final DeliveryPlan updatedPlan = currentPlan.mergeWithNextDeliveryPlan(updatedDeliveryPlan); - deliveryPlans.remove(currentPlan); - deliveryPlans.add(updatedPlan); - } - } - - /** - * Remove stale {@link LineItemStatus} from statistic. - */ - public void cleanLineItemStatuses(ZonedDateTime now, long lineItemStatusTtl, int maxPlanNumberInDeliveryProgress) { - lineItemStatuses.entrySet().removeIf(entry -> isLineItemStatusExpired(entry.getKey(), now, lineItemStatusTtl)); - - lineItemStatuses.values().forEach( - lineItemStatus -> cutCachedDeliveryPlans(lineItemStatus, maxPlanNumberInDeliveryProgress)); - } - - /** - * Returns true when lineItem is not in metaData and it is expired for more then defined in configuration time. - */ - private boolean isLineItemStatusExpired(String lineItemId, ZonedDateTime now, long lineItemStatusTtl) { - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - - return lineItem == null || ChronoUnit.MILLIS.between(lineItem.getEndTimeStamp(), now) > lineItemStatusTtl; - } - - /** - * Cuts number of plans in {@link LineItemStatus} from overall statistic by number defined in configuration. - */ - private void cutCachedDeliveryPlans(LineItemStatus lineItemStatus, int maxPlanNumberInDeliveryProgress) { - final Set deliveryPlans = lineItemStatus.getDeliveryPlans(); - if (deliveryPlans.size() > maxPlanNumberInDeliveryProgress) { - final Set plansToRemove = deliveryPlans.stream() - .sorted(Comparator.comparing(DeliveryPlan::getEndTimeStamp)) - .limit(deliveryPlans.size() - maxPlanNumberInDeliveryProgress) - .collect(Collectors.toSet()); - plansToRemove.forEach(deliveryPlans::remove); - } - } - - /** - * Updates {@link LineItemStatus} with active {@link DeliveryPlan}. - */ - private void updateLineItemStatusWithActiveDeliveryPlan(LineItemStatus lineItemStatus, - DeliveryPlan updatedDeliveryPlan) { - final Set deliveryPlans = lineItemStatus.getDeliveryPlans(); - final DeliveryPlan currentPlan = deliveryPlans.stream() - .filter(plan -> Objects.equals(plan.getPlanId(), updatedDeliveryPlan.getPlanId())) - .filter(plan -> plan.isUpdated(updatedDeliveryPlan.getDeliverySchedule())) - .findAny() - .orElse(null); - if (currentPlan != null) { - if (!Objects.equals(currentPlan.getUpdatedTimeStamp(), updatedDeliveryPlan.getUpdatedTimeStamp())) { - deliveryPlans.add(updatedDeliveryPlan); - deliveryPlans.remove(currentPlan); - } - } else { - deliveryPlans.add(updatedDeliveryPlan); - } - } - - public void updateWithActiveLineItems(Collection lineItems) { - lineItems.forEach(lineItem -> lineItemStatuses.putIfAbsent(lineItem.getLineItemId(), - createLineItemStatus(lineItem.getLineItemId()))); - } - - public Map getLineItemStatuses() { - return lineItemStatuses; - } - - public Map getRequestsPerAccount() { - return requestsPerAccount; - } - - public Map> getLineItemIdToLost() { - return lineItemIdToLost; - } - - public LongAdder getRequests() { - return requests; - } - - public ZonedDateTime getStartTimeStamp() { - return startTimeStamp; - } - - public void setStartTimeStamp(ZonedDateTime startTimeStamp) { - this.startTimeStamp = startTimeStamp; - } - - public void setEndTimeStamp(ZonedDateTime endTimeStamp) { - this.endTimeStamp = endTimeStamp; - } - - public ZonedDateTime getEndTimeStamp() { - return endTimeStamp; - } - - private LongAdder accountRequests(String account) { - return requestsPerAccount.computeIfAbsent(account, ignored -> new LongAdder()); - } - - /** - * Increments {@link LineItemStatus} metric, creates line item status if does not exist. - */ - private void increment(String lineItemId, Consumer inc) { - inc.accept(lineItemStatuses.computeIfAbsent(lineItemId, this::createLineItemStatus)); - } - - /** - * Increment tokens in active delivery report. - */ - private void incToken(String lineItemId, Map planIdToTokenPriority) { - final LineItemStatus lineItemStatus = lineItemStatuses.get(lineItemId); - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - final DeliveryPlan lineItemActivePlan = lineItem.getActiveDeliveryPlan(); - if (lineItemActivePlan != null) { - DeliveryPlan reportActivePlan = lineItemStatus.getDeliveryPlans().stream() - .filter(plan -> Objects.equals(plan.getPlanId(), lineItemActivePlan.getPlanId())) - .findFirst() - .orElse(null); - if (reportActivePlan == null) { - reportActivePlan = lineItemActivePlan.withoutSpentTokens(); - lineItemStatus.getDeliveryPlans().add(reportActivePlan); - } - - final Integer tokenPriority = planIdToTokenPriority.get(reportActivePlan.getPlanId()); - if (tokenPriority != null) { - reportActivePlan.incTokenWithPriority(tokenPriority); - } - } - } - - /** - * Updates lostToLineItem metric for line item specified by lineItemId parameter against line item ids from - * parameter lostToLineItemIds - */ - private void updateLostToEachLineItem(String lineItemId, Set lostToLineItemsIds, - Map> lostToLineItemTimes) { - final Map lostToLineItemsTimes = lostToLineItemTimes - .computeIfAbsent(lineItemId, key -> new ConcurrentHashMap<>()); - lostToLineItemsIds.forEach(lostToLineItemId -> incLostToLineItemTimes(lostToLineItemId, lostToLineItemsTimes)); - } - - /** - * Updates listToLineItem metric against line item specified in parameter lostToLineItemId - */ - private void incLostToLineItemTimes(String lostToLineItemId, Map lostToLineItemsTimes) { - final LostToLineItem lostToLineItem = lostToLineItemsTimes.computeIfAbsent(lostToLineItemId, - ignored -> LostToLineItem.of(lostToLineItemId, new LongAdder())); - lostToLineItem.getCount().increment(); - } - - /** - * Merges requests per account to overall statistics. - */ - private void mergeRequestsCount(String accountId, LongAdder requestsCount, - Map requestsPerAccount) { - requestsPerAccount.computeIfPresent(accountId, (key, oldValue) -> { - oldValue.add(requestsCount.sum()); - return oldValue; - }); - requestsPerAccount.putIfAbsent(accountId, requestsCount); - } - - private void mergeCurrentLineItemLostReportToOverall( - String lineItemId, - Map currentLineItemLost, - Map> overallLineItemIdToLost) { - final Map overallLineItemLost = overallLineItemIdToLost - .computeIfAbsent(lineItemId, ignored -> new ConcurrentHashMap<>()); - currentLineItemLost.forEach((lineItemIdLostTo, currentLostToLineItem) -> - overallLineItemLost.merge(lineItemIdLostTo, currentLostToLineItem, this::addToCount) - ); - } - - private LostToLineItem addToCount(LostToLineItem mergeTo, LostToLineItem mergeFrom) { - mergeTo.getCount().add(mergeFrom.getCount().sum()); - return mergeTo; - } -} diff --git a/src/main/java/org/prebid/server/deals/lineitem/DeliveryToken.java b/src/main/java/org/prebid/server/deals/lineitem/DeliveryToken.java deleted file mode 100644 index 67188cc96e9..00000000000 --- a/src/main/java/org/prebid/server/deals/lineitem/DeliveryToken.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.prebid.server.deals.lineitem; - -import org.prebid.server.deals.proto.Token; - -import java.util.Comparator; -import java.util.Objects; -import java.util.concurrent.atomic.LongAdder; - -public class DeliveryToken implements Comparable { - - private static final Comparator COMPARATOR = Comparator.comparing(DeliveryToken::getPriorityClass); - - private final Token token; - - private final LongAdder spent; - - private DeliveryToken(Token token) { - this(token, new LongAdder()); - } - - private DeliveryToken(Token token, LongAdder spent) { - this.token = Objects.requireNonNull(token); - this.spent = Objects.requireNonNull(spent); - } - - public static DeliveryToken of(DeliveryToken deliveryToken) { - return new DeliveryToken(deliveryToken.token); - } - - public static DeliveryToken of(Token token) { - return new DeliveryToken(token); - } - - /** - * Return unspent tokens from {@link DeliveryToken}. - */ - public int getUnspent() { - return (int) (token.getTotal() - spent.sum()); - } - - public void inc() { - spent.increment(); - } - - public DeliveryToken mergeWithToken(Token nextToken, boolean sumTotal) { - if (nextToken == null) { - return this; - } else { - final int total = sumTotal - ? getTotal() + nextToken.getTotal() - : nextToken.getTotal(); - return new DeliveryToken(Token.of(getPriorityClass(), total), spent); - } - } - - public LongAdder getSpent() { - return spent; - } - - public Integer getTotal() { - return token.getTotal(); - } - - public Integer getPriorityClass() { - return token.getPriorityClass(); - } - - @Override - public int compareTo(DeliveryToken another) { - return COMPARATOR.compare(this, another); - } -} diff --git a/src/main/java/org/prebid/server/deals/lineitem/LineItem.java b/src/main/java/org/prebid/server/deals/lineitem/LineItem.java deleted file mode 100644 index 58554ab6cdd..00000000000 --- a/src/main/java/org/prebid/server/deals/lineitem/LineItem.java +++ /dev/null @@ -1,276 +0,0 @@ -package org.prebid.server.deals.lineitem; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; -import org.prebid.server.deals.proto.DeliverySchedule; -import org.prebid.server.deals.proto.FrequencyCap; -import org.prebid.server.deals.proto.LineItemMetaData; -import org.prebid.server.deals.proto.LineItemSize; -import org.prebid.server.deals.proto.Price; -import org.prebid.server.deals.targeting.TargetingDefinition; - -import java.math.BigDecimal; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -public class LineItem { - - private static final Logger logger = LoggerFactory.getLogger(LineItem.class); - - private final LineItemMetaData metaData; - - private final Price normalizedPrice; - - private final List fcapIds; - - private final TargetingDefinition targetingDefinition; - - private final AtomicReference activeDeliveryPlan; - - private final AtomicReference readyAt; - - private LineItem(LineItemMetaData metaData, Price normalizedPrice, TargetingDefinition targetingDefinition) { - this.metaData = Objects.requireNonNull(metaData); - this.normalizedPrice = normalizedPrice; - this.targetingDefinition = targetingDefinition; - - this.fcapIds = extractFcapIds(metaData); - - activeDeliveryPlan = new AtomicReference<>(); - readyAt = new AtomicReference<>(); - } - - private LineItem(LineItemMetaData metaData, - Price normalizedPrice, - TargetingDefinition targetingDefinition, - ZonedDateTime readyAt, - ZonedDateTime now, - DeliveryPlan currentPlan) { - this(metaData, normalizedPrice, targetingDefinition); - this.readyAt.set(readyAt); - - updateOrAdvanceActivePlan(now, true, currentPlan); - } - - private LineItem(LineItemMetaData metaData, - Price normalizedPrice, - TargetingDefinition targetingDefinition, - ZonedDateTime now) { - this(metaData, normalizedPrice, targetingDefinition, null, now, null); - } - - public static LineItem of(LineItemMetaData metaData, - Price normalizedPrice, - TargetingDefinition targetingDefinition, - ZonedDateTime now) { - return new LineItem(metaData, normalizedPrice, targetingDefinition, now); - } - - public LineItem withUpdatedMetadata(LineItemMetaData metaData, - Price normalizedPrice, - TargetingDefinition targetingDefinition, - ZonedDateTime readyAt, - ZonedDateTime now) { - return new LineItem(metaData, normalizedPrice, targetingDefinition, readyAt, now, getActiveDeliveryPlan()); - } - - public void advanceToNextPlan(ZonedDateTime now, boolean isPlannerResponsive) { - updateOrAdvanceActivePlan(now, isPlannerResponsive, getActiveDeliveryPlan()); - } - - /** - * Increments tokens in {@link DeliveryToken} with highest priority within {@link DeliveryPlan}. - * - * @return class of the token incremented. - */ - public Integer incSpentToken(ZonedDateTime now) { - return incSpentToken(now, 0); - } - - public Integer incSpentToken(ZonedDateTime now, long adjustment) { - final DeliveryPlan deliveryPlan = activeDeliveryPlan.get(); - - if (deliveryPlan != null) { - final Integer tokenClassIncremented = deliveryPlan.incSpentToken(); - ZonedDateTime readyAtNewValue = deliveryPlan.calculateReadyAt(); - readyAtNewValue = readyAtNewValue != null && adjustment != 0 - ? readyAtNewValue.plusNanos(TimeUnit.MILLISECONDS.toNanos(adjustment)) - : readyAtNewValue; - readyAt.set(readyAtNewValue); - if (logger.isDebugEnabled()) { - logger.debug("ReadyAt for lineItem {0} plan {1} was updated to {2} after token was spent. Total number" - + " of unspent token is {3}. Current time is {4}", - getLineItemId(), deliveryPlan.getPlanId(), - readyAt.get(), deliveryPlan.getUnspentTokens(), now); - } - return tokenClassIncremented; - } - return null; - } - - public Integer getHighestUnspentTokensClass() { - final DeliveryPlan activeDeliveryPlan = getActiveDeliveryPlan(); - return activeDeliveryPlan != null ? activeDeliveryPlan.getHighestUnspentTokensClass() : null; - } - - public boolean isActive(ZonedDateTime now) { - return dateBetween(now, metaData.getStartTimeStamp(), metaData.getEndTimeStamp()); - } - - public DeliveryPlan getActiveDeliveryPlan() { - return activeDeliveryPlan.get(); - } - - public ZonedDateTime getReadyAt() { - return readyAt.get(); - } - - public BigDecimal getCpm() { - if (normalizedPrice != null) { - return normalizedPrice.getCpm(); - } - return null; - } - - public String getCurrency() { - if (normalizedPrice != null) { - return normalizedPrice.getCurrency(); - } - return null; - } - - public String getLineItemId() { - return metaData.getLineItemId(); - } - - public String getExtLineItemId() { - return metaData.getExtLineItemId(); - } - - public String getDealId() { - return metaData.getDealId(); - } - - public String getAccountId() { - return metaData.getAccountId(); - } - - public String getSource() { - return metaData.getSource(); - } - - public Integer getRelativePriority() { - return metaData.getRelativePriority(); - } - - public ZonedDateTime getEndTimeStamp() { - return metaData.getEndTimeStamp(); - } - - public ZonedDateTime getStartTimeStamp() { - return metaData.getStartTimeStamp(); - } - - public ZonedDateTime getUpdatedTimeStamp() { - return metaData.getUpdatedTimeStamp(); - } - - public List getFrequencyCaps() { - return metaData.getFrequencyCaps(); - } - - public List getSizes() { - return metaData.getSizes(); - } - - public ObjectNode getTargeting() { - return metaData.getTargeting(); - } - - public TargetingDefinition getTargetingDefinition() { - return targetingDefinition; - } - - public List getFcapIds() { - return fcapIds; - } - - private static List extractFcapIds(LineItemMetaData metaData) { - return CollectionUtils.emptyIfNull(metaData.getFrequencyCaps()).stream() - .map(FrequencyCap::getFcapId) - .toList(); - } - - private void updateOrAdvanceActivePlan(ZonedDateTime now, boolean isPlannerResponsive, DeliveryPlan currentPlan) { - final DeliverySchedule currentSchedule = ListUtils.emptyIfNull(metaData.getDeliverySchedules()).stream() - .filter(schedule -> dateBetween(now, schedule.getStartTimeStamp(), schedule.getEndTimeStamp())) - .findFirst() - .orElse(null); - - if (currentSchedule != null) { - final DeliveryPlan resolvedPlan = resolveActivePlan(currentPlan, currentSchedule, isPlannerResponsive); - final ZonedDateTime readyAtBeforeUpdate = readyAt.get(); - if (currentPlan != resolvedPlan) { - readyAt.set(currentPlan == null || !Objects.equals(currentSchedule.getPlanId(), currentPlan.getPlanId()) - ? calculateReadyAfterMovingToNextPlan(now, resolvedPlan) - : calculateReadyAtAfterPlanUpdated(now, resolvedPlan)); - logger.info("ReadyAt for Line Item `{0}` was updated from plan {1} to {2} and readyAt from {3} to {4}" - + " at time is {5}", getLineItemId(), - currentPlan != null ? currentPlan.getPlanId() : " no plan ", resolvedPlan.getPlanId(), - readyAtBeforeUpdate, getReadyAt(), now); - if (logger.isDebugEnabled()) { - logger.debug("Unspent tokens number for plan {0} is {1}", resolvedPlan.getPlanId(), - resolvedPlan.getUnspentTokens()); - } - } - activeDeliveryPlan.set(resolvedPlan); - } else { - activeDeliveryPlan.set(null); - readyAt.set(null); - logger.info("Active plan for Line Item `{0}` was not found at time is {1}, readyAt updated with 'never'," - + " until active plan become available", getLineItemId(), now); - } - } - - private ZonedDateTime calculateReadyAtAfterPlanUpdated(ZonedDateTime now, DeliveryPlan resolvedPlan) { - final ZonedDateTime resolvedReadyAt = resolvedPlan.calculateReadyAt(); - logger.debug("Current plan for Line Item `{0}` was considered as updated from GP response and readyAt will be " - + "updated from {1} to {2} at time is {3}", getLineItemId(), getReadyAt(), resolvedReadyAt, now); - return resolvedReadyAt; - } - - private ZonedDateTime calculateReadyAfterMovingToNextPlan(ZonedDateTime now, DeliveryPlan resolvedPlan) { - return resolvedPlan.getDeliveryTokens().stream().anyMatch(deliveryToken -> deliveryToken.getTotal() > 0) - ? now - : null; - } - - private static DeliveryPlan resolveActivePlan(DeliveryPlan currentPlan, - DeliverySchedule currentSchedule, - boolean isPlannerResponsive) { - if (currentPlan != null) { - if (Objects.equals(currentPlan.getPlanId(), currentSchedule.getPlanId())) { - return currentPlan.getUpdatedTimeStamp().isBefore(currentSchedule.getUpdatedTimeStamp()) - ? currentPlan.mergeWithNextDeliverySchedule(currentSchedule, false) - : currentPlan; - } else if (!isPlannerResponsive) { - return currentPlan.mergeWithNextDeliverySchedule(currentSchedule, true); - } - } - - return DeliveryPlan.of(currentSchedule); - } - - /** - * Returns true when now parameter is after startDate and before expirationDate. - */ - private static boolean dateBetween(ZonedDateTime now, ZonedDateTime startDate, ZonedDateTime expirationDate) { - return (now.isEqual(startDate) || now.isAfter(startDate)) && now.isBefore(expirationDate); - } -} diff --git a/src/main/java/org/prebid/server/deals/lineitem/LineItemStatus.java b/src/main/java/org/prebid/server/deals/lineitem/LineItemStatus.java deleted file mode 100644 index e05cd399d0d..00000000000 --- a/src/main/java/org/prebid/server/deals/lineitem/LineItemStatus.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.prebid.server.deals.lineitem; - -import io.vertx.core.impl.ConcurrentHashSet; -import lombok.Value; -import org.prebid.server.deals.proto.report.Event; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Value -public class LineItemStatus { - - String lineItemId; - - String source; - - String dealId; - - String extLineItemId; - - String accountId; - - LongAdder domainMatched; - - LongAdder targetMatched; - - LongAdder targetMatchedButFcapped; - - LongAdder targetMatchedButFcapLookupFailed; - - LongAdder pacingDeferred; - - LongAdder sentToBidder; - - LongAdder sentToBidderAsTopMatch; - - LongAdder receivedFromBidder; - - LongAdder receivedFromBidderInvalidated; - - LongAdder sentToClient; - - LongAdder sentToClientAsTopMatch; - - Set lostToLineItems; - - Set events; - - Set deliveryPlans; - - private LineItemStatus(String lineItemId, String source, String dealId, String extLineItemId, String accountId) { - this.lineItemId = lineItemId; - this.source = source; - this.dealId = dealId; - this.extLineItemId = extLineItemId; - this.accountId = accountId; - - domainMatched = new LongAdder(); - targetMatched = new LongAdder(); - targetMatchedButFcapped = new LongAdder(); - targetMatchedButFcapLookupFailed = new LongAdder(); - pacingDeferred = new LongAdder(); - sentToBidder = new LongAdder(); - sentToBidderAsTopMatch = new LongAdder(); - receivedFromBidder = new LongAdder(); - receivedFromBidderInvalidated = new LongAdder(); - sentToClient = new LongAdder(); - sentToClientAsTopMatch = new LongAdder(); - - lostToLineItems = new ConcurrentHashSet<>(); - events = new ConcurrentHashSet<>(); - deliveryPlans = new ConcurrentHashSet<>(); - } - - public static LineItemStatus of(String lineItemId, String source, String dealId, String extLineItemId, - String accountId) { - return new LineItemStatus(lineItemId, source, dealId, extLineItemId, accountId); - } - - public static LineItemStatus of(LineItem lineItem) { - return new LineItemStatus(lineItem.getLineItemId(), lineItem.getSource(), lineItem.getDealId(), - lineItem.getExtLineItemId(), lineItem.getAccountId()); - } - - public static LineItemStatus of(String lineItemId) { - return new LineItemStatus(lineItemId, null, null, null, null); - } - - public void incDomainMatched() { - domainMatched.increment(); - } - - public void incTargetMatched() { - targetMatched.increment(); - } - - public void incTargetMatchedButFcapped() { - targetMatchedButFcapped.increment(); - } - - public void incTargetMatchedButFcapLookupFailed() { - targetMatchedButFcapLookupFailed.increment(); - } - - public void incPacingDeferred() { - pacingDeferred.increment(); - } - - public void incSentToBidder() { - sentToBidder.increment(); - } - - public void incSentToBidderAsTopMatch() { - sentToBidderAsTopMatch.increment(); - } - - public void incReceivedFromBidder() { - receivedFromBidder.increment(); - } - - public void incReceivedFromBidderInvalidated() { - receivedFromBidderInvalidated.increment(); - } - - public void incSentToClient() { - sentToClient.increment(); - } - - public void incSentToClientAsTopMatch() { - sentToClientAsTopMatch.increment(); - } - - public void merge(LineItemStatus other) { - domainMatched.add(other.domainMatched.sum()); - targetMatched.add(other.targetMatched.sum()); - targetMatchedButFcapped.add(other.targetMatchedButFcapped.sum()); - targetMatchedButFcapLookupFailed.add(other.getTargetMatchedButFcapLookupFailed().sum()); - pacingDeferred.add(other.pacingDeferred.sum()); - sentToBidder.add(other.sentToBidder.sum()); - sentToBidderAsTopMatch.add(other.sentToBidderAsTopMatch.sum()); - receivedFromBidder.add(other.receivedFromBidder.sum()); - receivedFromBidderInvalidated.add(other.receivedFromBidderInvalidated.sum()); - sentToClient.add(other.sentToClient.sum()); - sentToClientAsTopMatch.add(other.sentToClientAsTopMatch.sum()); - mergeEvents(other); - } - - private void mergeEvents(LineItemStatus other) { - final Map typesToEvent = other.events.stream() - .collect(Collectors.toMap(Event::getType, Function.identity())); - typesToEvent.forEach(this::addOrUpdateEvent); - } - - private void addOrUpdateEvent(String type, Event distEvent) { - final Event sameTypeEvent = events.stream() - .filter(event -> event.getType().equals(type)) - .findFirst().orElse(null); - if (sameTypeEvent != null) { - sameTypeEvent.getCount().add(distEvent.getCount().sum()); - } else { - events.add(distEvent); - } - } -} diff --git a/src/main/java/org/prebid/server/deals/lineitem/LostToLineItem.java b/src/main/java/org/prebid/server/deals/lineitem/LostToLineItem.java deleted file mode 100644 index 0b7be6d1f21..00000000000 --- a/src/main/java/org/prebid/server/deals/lineitem/LostToLineItem.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.lineitem; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.concurrent.atomic.LongAdder; - -@AllArgsConstructor(staticName = "of") -@Value -public class LostToLineItem { - - String lineItemId; - - LongAdder count; -} diff --git a/src/main/java/org/prebid/server/deals/model/AdminAccounts.java b/src/main/java/org/prebid/server/deals/model/AdminAccounts.java deleted file mode 100644 index b5829dab3e6..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AdminAccounts.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@Value -@AllArgsConstructor(staticName = "of") -public class AdminAccounts { - - List accounts; -} diff --git a/src/main/java/org/prebid/server/deals/model/AdminCentralResponse.java b/src/main/java/org/prebid/server/deals/model/AdminCentralResponse.java deleted file mode 100644 index 034389ddd78..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AdminCentralResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class AdminCentralResponse { - - LogTracer tracer; - - @JsonProperty("storedrequest") - Command storedRequest; - - @JsonProperty("storedrequest-amp") - Command storedRequestAmp; - - @JsonProperty("line-items") - Command lineItems; - - Command account; - - ServicesCommand services; -} diff --git a/src/main/java/org/prebid/server/deals/model/AdminLineItems.java b/src/main/java/org/prebid/server/deals/model/AdminLineItems.java deleted file mode 100644 index 14f2ecca150..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AdminLineItems.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@Value -@AllArgsConstructor(staticName = "of") -public class AdminLineItems { - - List ids; -} diff --git a/src/main/java/org/prebid/server/deals/model/AlertEvent.java b/src/main/java/org/prebid/server/deals/model/AlertEvent.java deleted file mode 100644 index 157ab84c41f..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AlertEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.Value; - -import java.time.ZonedDateTime; - -@Builder -@Value -public class AlertEvent { - - String id; - - String action; - - AlertPriority priority; - - ZonedDateTime updatedAt; - - String name; - - String details; - - AlertSource source; -} diff --git a/src/main/java/org/prebid/server/deals/model/AlertPriority.java b/src/main/java/org/prebid/server/deals/model/AlertPriority.java deleted file mode 100644 index 074fa5e164c..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AlertPriority.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.deals.model; - -public enum AlertPriority { - - HIGH, MEDIUM, LOW -} diff --git a/src/main/java/org/prebid/server/deals/model/AlertProxyProperties.java b/src/main/java/org/prebid/server/deals/model/AlertProxyProperties.java deleted file mode 100644 index 28d076129da..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AlertProxyProperties.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.Value; - -import java.util.Map; - -@Builder -@Value -public class AlertProxyProperties { - - boolean enabled; - - String url; - - int timeoutSec; - - Map alertTypes; - - String username; - - String password; -} diff --git a/src/main/java/org/prebid/server/deals/model/AlertSource.java b/src/main/java/org/prebid/server/deals/model/AlertSource.java deleted file mode 100644 index a673a18cb5b..00000000000 --- a/src/main/java/org/prebid/server/deals/model/AlertSource.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -@Builder -@Value -public class AlertSource { - - String env; - - @JsonProperty("data-center") - String dataCenter; - - String region; - - String system; - - @JsonProperty("sub-system") - String subSystem; - - @JsonProperty("host-id") - String hostId; -} diff --git a/src/main/java/org/prebid/server/deals/model/Command.java b/src/main/java/org/prebid/server/deals/model/Command.java deleted file mode 100644 index 7acbec5e4c0..00000000000 --- a/src/main/java/org/prebid/server/deals/model/Command.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class Command { - - String cmd; - - ObjectNode body; -} diff --git a/src/main/java/org/prebid/server/deals/model/DeepDebugLog.java b/src/main/java/org/prebid/server/deals/model/DeepDebugLog.java deleted file mode 100644 index 597c55f63b0..00000000000 --- a/src/main/java/org/prebid/server/deals/model/DeepDebugLog.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.prebid.server.deals.model; - -import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; -import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.function.Supplier; - -public class DeepDebugLog { - - private final boolean deepDebugEnabled; - - private final List entries; - - private final Clock clock; - - private DeepDebugLog(boolean deepDebugEnabled, Clock clock) { - this.deepDebugEnabled = deepDebugEnabled; - this.entries = deepDebugEnabled ? new LinkedList<>() : null; - this.clock = Objects.requireNonNull(clock); - } - - public static DeepDebugLog create(boolean deepDebugEnabled, Clock clock) { - return new DeepDebugLog(deepDebugEnabled, clock); - } - - public void add(String lineItemId, Category category, Supplier messageSupplier) { - if (deepDebugEnabled) { - entries.add(ExtTraceDeal.of(lineItemId, ZonedDateTime.now(clock), category, messageSupplier.get())); - } - } - - public boolean isDeepDebugEnabled() { - return deepDebugEnabled; - } - - public List entries() { - return entries == null ? Collections.emptyList() : Collections.unmodifiableList(entries); - } -} diff --git a/src/main/java/org/prebid/server/deals/model/DeliveryProgressProperties.java b/src/main/java/org/prebid/server/deals/model/DeliveryProgressProperties.java deleted file mode 100644 index 95007f33ea0..00000000000 --- a/src/main/java/org/prebid/server/deals/model/DeliveryProgressProperties.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class DeliveryProgressProperties { - - long lineItemStatusTtlSeconds; - - int cachedPlansNumber; -} diff --git a/src/main/java/org/prebid/server/deals/model/DeliveryStatsProperties.java b/src/main/java/org/prebid/server/deals/model/DeliveryStatsProperties.java deleted file mode 100644 index 237c094a33b..00000000000 --- a/src/main/java/org/prebid/server/deals/model/DeliveryStatsProperties.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Builder -@Value -public class DeliveryStatsProperties { - - @NonNull - String endpoint; - - int cachedReportsNumber; - - long timeoutMs; - - int lineItemsPerReport; - - int reportsIntervalMs; - - int batchesIntervalMs; - - boolean requestCompressionEnabled; - - @NonNull - String username; - - @NonNull - String password; -} diff --git a/src/main/java/org/prebid/server/deals/model/DeploymentProperties.java b/src/main/java/org/prebid/server/deals/model/DeploymentProperties.java deleted file mode 100644 index afd08710145..00000000000 --- a/src/main/java/org/prebid/server/deals/model/DeploymentProperties.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.Value; - -@Builder -@Value -public class DeploymentProperties { - - String pbsHostId; - - String pbsRegion; - - String pbsVendor; - - String profile; - - String infra; - - String dataCenter; - - String system; - - String subSystem; -} diff --git a/src/main/java/org/prebid/server/deals/model/DeviceInfo.java b/src/main/java/org/prebid/server/deals/model/DeviceInfo.java deleted file mode 100644 index e58f9bcedf9..00000000000 --- a/src/main/java/org/prebid/server/deals/model/DeviceInfo.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Builder -@Value -public class DeviceInfo { - - @NonNull - String vendor; - - DeviceType deviceType; - - String deviceTypeRaw; - - String osfamily; - - String os; - - String osVersion; - - String manufacturer; - - String model; - - String browser; - - String browserVersion; - - String carrier; - - String language; -} diff --git a/src/main/java/org/prebid/server/deals/model/DeviceType.java b/src/main/java/org/prebid/server/deals/model/DeviceType.java deleted file mode 100644 index 04acc900bce..00000000000 --- a/src/main/java/org/prebid/server/deals/model/DeviceType.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.prebid.server.deals.model; - -public enum DeviceType { - - MOBILE("mobile"), - DESKTOP("desktop"), - TV("connected tv"), - PHONE("phone"), - DEVICE("connected device"), - SET_TOP_BOX("set top box"), - TABLET("tablet"); - - private final String name; - - DeviceType(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public static DeviceType resolveDeviceType(String deviceType) { - if (deviceType == null) { - return null; - } - - return switch (deviceType) { - case "Mobile Phone", "Mobile", "SmartPhone", "SmallScreen" -> MOBILE; - case "Desktop", "Single-board Computer" -> DESKTOP; - case "TV", "Tv" -> TV; - case "Fixed Wireless Phone", "Vehicle Phone" -> PHONE; - case "Tablet" -> TABLET; - case "Digital Home Assistant", "Digital Signage Media Player", - "eReader", "EReader", "Console", "Games Console", "Media Player", - "Payment Terminal", "Refrigerator", "Vehicle Multimedia System", - "Weighing Scale", "Wristwatch", "SmartWatch" -> DEVICE; - // might not be correct for 51degrees (https://51degrees.com/resources/property-dictionary) - case "Set Top Box", "MediaHub" -> SET_TOP_BOX; - default -> null; - }; - } -} diff --git a/src/main/java/org/prebid/server/deals/model/ExtUser.java b/src/main/java/org/prebid/server/deals/model/ExtUser.java deleted file mode 100644 index 26f6d610fa3..00000000000 --- a/src/main/java/org/prebid/server/deals/model/ExtUser.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class ExtUser { - - @JsonProperty("fcapIds") - List fcapIds; -} diff --git a/src/main/java/org/prebid/server/deals/model/LogCriteriaFilter.java b/src/main/java/org/prebid/server/deals/model/LogCriteriaFilter.java deleted file mode 100644 index 758a40a6282..00000000000 --- a/src/main/java/org/prebid/server/deals/model/LogCriteriaFilter.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class LogCriteriaFilter { - - @JsonProperty("accountId") - String accountId; - - @JsonProperty("bidderCode") - String bidderCode; - - @JsonProperty("lineItemId") - String lineItemId; -} diff --git a/src/main/java/org/prebid/server/deals/model/LogTracer.java b/src/main/java/org/prebid/server/deals/model/LogTracer.java deleted file mode 100644 index fb5c2e9c456..00000000000 --- a/src/main/java/org/prebid/server/deals/model/LogTracer.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class LogTracer { - - String cmd; - - Boolean raw; - - @JsonProperty("durationInSeconds") - Long durationInSeconds; - - LogCriteriaFilter filters; -} diff --git a/src/main/java/org/prebid/server/deals/model/MatchLineItemsResult.java b/src/main/java/org/prebid/server/deals/model/MatchLineItemsResult.java deleted file mode 100644 index 22d1cd01078..00000000000 --- a/src/main/java/org/prebid/server/deals/model/MatchLineItemsResult.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; -import org.prebid.server.deals.lineitem.LineItem; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class MatchLineItemsResult { - - List lineItems; -} diff --git a/src/main/java/org/prebid/server/deals/model/PlannerProperties.java b/src/main/java/org/prebid/server/deals/model/PlannerProperties.java deleted file mode 100644 index f7eac001842..00000000000 --- a/src/main/java/org/prebid/server/deals/model/PlannerProperties.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Builder -@Value -public class PlannerProperties { - - @NonNull - String planEndpoint; - - @NonNull - String registerEndpoint; - - long timeoutMs; - - long registerPeriodSeconds; - - @NonNull - String username; - - @NonNull - String password; -} diff --git a/src/main/java/org/prebid/server/deals/model/Segment.java b/src/main/java/org/prebid/server/deals/model/Segment.java deleted file mode 100644 index 648e3cf5f0c..00000000000 --- a/src/main/java/org/prebid/server/deals/model/Segment.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class Segment { - - String id; -} diff --git a/src/main/java/org/prebid/server/deals/model/ServicesCommand.java b/src/main/java/org/prebid/server/deals/model/ServicesCommand.java deleted file mode 100644 index f58c26791d5..00000000000 --- a/src/main/java/org/prebid/server/deals/model/ServicesCommand.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class ServicesCommand { - - String cmd; -} diff --git a/src/main/java/org/prebid/server/deals/model/SimulationProperties.java b/src/main/java/org/prebid/server/deals/model/SimulationProperties.java deleted file mode 100644 index fc520afbbcf..00000000000 --- a/src/main/java/org/prebid/server/deals/model/SimulationProperties.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.Builder; -import lombok.Value; - -@Builder -@Value -public class SimulationProperties { - - boolean enabled; - - boolean winEventsEnabled; - - boolean userDetailsEnabled; -} diff --git a/src/main/java/org/prebid/server/deals/model/TxnLog.java b/src/main/java/org/prebid/server/deals/model/TxnLog.java deleted file mode 100644 index f4a4ae34c48..00000000000 --- a/src/main/java/org/prebid/server/deals/model/TxnLog.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import lombok.experimental.FieldDefaults; -import org.apache.commons.collections4.Factory; -import org.apache.commons.collections4.MapUtils; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -@Getter -@Accessors(fluent = true, chain = true) -@NoArgsConstructor(staticName = "create") -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -@EqualsAndHashCode -public class TxnLog { - - Set lineItemsMatchedDomainTargeting = new HashSet<>(); - - Set lineItemsMatchedWholeTargeting = new HashSet<>(); - - Set lineItemsMatchedTargetingFcapped = new HashSet<>(); - - Set lineItemsMatchedTargetingFcapLookupFailed = new HashSet<>(); - - Set lineItemsReadyToServe = new HashSet<>(); - - Set lineItemsPacingDeferred = new HashSet<>(); - - Map> lineItemsSentToBidder = MapUtils.lazyMap( - new TreeMap<>(String.CASE_INSENSITIVE_ORDER), - (Factory>) HashSet::new); - - Map> lineItemsSentToBidderAsTopMatch = MapUtils.lazyMap( - new TreeMap<>(String.CASE_INSENSITIVE_ORDER), - (Factory>) HashSet::new); - - Map> lineItemsReceivedFromBidder = MapUtils.lazyMap( - new TreeMap<>(String.CASE_INSENSITIVE_ORDER), - (Factory>) HashSet::new); - - Set lineItemsResponseInvalidated = new HashSet<>(); - - Set lineItemsSentToClient = new HashSet<>(); - - Map> lostMatchingToLineItems = MapUtils.lazyMap(new HashMap<>(), - (Factory>) HashSet::new); - - Map> lostAuctionToLineItems = MapUtils.lazyMap(new HashMap<>(), - (Factory>) HashSet::new); - - Set lineItemSentToClientAsTopMatch = new HashSet<>(); -} diff --git a/src/main/java/org/prebid/server/deals/model/User.java b/src/main/java/org/prebid/server/deals/model/User.java deleted file mode 100644 index fcb777fb957..00000000000 --- a/src/main/java/org/prebid/server/deals/model/User.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class User { - - List data; - - ExtUser ext; -} diff --git a/src/main/java/org/prebid/server/deals/model/UserData.java b/src/main/java/org/prebid/server/deals/model/UserData.java deleted file mode 100644 index e25f41ab6ff..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserData.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserData { - - String id; - - String name; - - List segment; -} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetails.java b/src/main/java/org/prebid/server/deals/model/UserDetails.java deleted file mode 100644 index 5ded7d3a481..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserDetails.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserDetails { - - private static final UserDetails EMPTY = UserDetails.of(null, null); - - List userData; - - List fcapIds; - - public static UserDetails empty() { - return EMPTY; - } -} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetailsProperties.java b/src/main/java/org/prebid/server/deals/model/UserDetailsProperties.java deleted file mode 100644 index 2a01b7a1208..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserDetailsProperties.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.NonNull; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserDetailsProperties { - - @NonNull - String userDetailsEndpoint; - - @NonNull - String winEventEndpoint; - - long timeout; - - @NonNull - List userIds; -} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetailsRequest.java b/src/main/java/org/prebid/server/deals/model/UserDetailsRequest.java deleted file mode 100644 index b17091924a1..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserDetailsRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserDetailsRequest { - - String time; - - List ids; -} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetailsResponse.java b/src/main/java/org/prebid/server/deals/model/UserDetailsResponse.java deleted file mode 100644 index 09e83e5ef11..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserDetailsResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserDetailsResponse { - - User user; -} diff --git a/src/main/java/org/prebid/server/deals/model/UserId.java b/src/main/java/org/prebid/server/deals/model/UserId.java deleted file mode 100644 index 4ab19e907ba..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserId.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserId { - - String type; - - String id; -} diff --git a/src/main/java/org/prebid/server/deals/model/UserIdRule.java b/src/main/java/org/prebid/server/deals/model/UserIdRule.java deleted file mode 100644 index 674cd05f04b..00000000000 --- a/src/main/java/org/prebid/server/deals/model/UserIdRule.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.deals.model; - -import lombok.AllArgsConstructor; -import lombok.NonNull; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class UserIdRule { - - @NonNull - String type; - - @NonNull - String source; - - @NonNull - String location; -} diff --git a/src/main/java/org/prebid/server/deals/model/WinEventNotification.java b/src/main/java/org/prebid/server/deals/model/WinEventNotification.java deleted file mode 100644 index 06e4dbcd85f..00000000000 --- a/src/main/java/org/prebid/server/deals/model/WinEventNotification.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.deals.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; -import org.prebid.server.deals.proto.FrequencyCap; - -import java.time.ZonedDateTime; -import java.util.List; - -@Builder(toBuilder = true) -@Value -public class WinEventNotification { - - @JsonProperty("bidderCode") - String bidderCode; - - @JsonProperty("bidId") - String bidId; - - @JsonProperty("lineItemId") - String lineItemId; - - String region; - - @JsonProperty("userIds") - List userIds; - - @JsonProperty("winEventDateTime") - ZonedDateTime winEventDateTime; - - @JsonProperty("lineUpdatedDateTime") - ZonedDateTime lineUpdatedDateTime; - - @JsonProperty("frequencyCaps") - List frequencyCaps; -} diff --git a/src/main/java/org/prebid/server/deals/proto/CurrencyServiceState.java b/src/main/java/org/prebid/server/deals/proto/CurrencyServiceState.java deleted file mode 100644 index 937ccdc5c39..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/CurrencyServiceState.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class CurrencyServiceState { - - @JsonProperty("lastUpdate") - String lastUpdate; -} diff --git a/src/main/java/org/prebid/server/deals/proto/DeliverySchedule.java b/src/main/java/org/prebid/server/deals/proto/DeliverySchedule.java deleted file mode 100644 index 32f7b779fa2..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/DeliverySchedule.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.time.ZonedDateTime; -import java.util.Set; - -/** - * Defines the contract for lineItems[].deliverySchedules[]. - */ -@Builder -@Value -public class DeliverySchedule { - - @JsonProperty("planId") - String planId; - - @JsonProperty("startTimeStamp") - ZonedDateTime startTimeStamp; - - @JsonProperty("endTimeStamp") - ZonedDateTime endTimeStamp; - - @JsonProperty("updatedTimeStamp") - ZonedDateTime updatedTimeStamp; - - Set tokens; -} diff --git a/src/main/java/org/prebid/server/deals/proto/FrequencyCap.java b/src/main/java/org/prebid/server/deals/proto/FrequencyCap.java deleted file mode 100644 index 053a9331fab..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/FrequencyCap.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -/** - * Defines the contract for lineItems[].frequencyCap. - */ -@Builder -@Value -public class FrequencyCap { - - @JsonProperty("fcapId") - String fcapId; - - Long count; - - Integer periods; - - @JsonProperty("periodType") - String periodType; -} diff --git a/src/main/java/org/prebid/server/deals/proto/LineItemMetaData.java b/src/main/java/org/prebid/server/deals/proto/LineItemMetaData.java deleted file mode 100644 index 8bba4760ba1..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/LineItemMetaData.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Builder; -import lombok.Value; - -import java.time.ZonedDateTime; -import java.util.List; - -/** - * Defines the contract for lineItems[]. - */ -@Builder(toBuilder = true) -@Value -public class LineItemMetaData { - - @JsonProperty("lineItemId") - String lineItemId; - - @JsonProperty("extLineItemId") - String extLineItemId; - - @JsonProperty("dealId") - String dealId; - - List sizes; - - @JsonProperty("accountId") - String accountId; - - String source; - - Price price; - - @JsonProperty("relativePriority") - Integer relativePriority; - - @JsonProperty("startTimeStamp") - ZonedDateTime startTimeStamp; - - @JsonProperty("endTimeStamp") - ZonedDateTime endTimeStamp; - - @JsonProperty("updatedTimeStamp") - ZonedDateTime updatedTimeStamp; - - String status; - - @JsonProperty("frequencyCaps") - List frequencyCaps; - - @JsonProperty("deliverySchedules") - List deliverySchedules; - - ObjectNode targeting; -} diff --git a/src/main/java/org/prebid/server/deals/proto/LineItemSize.java b/src/main/java/org/prebid/server/deals/proto/LineItemSize.java deleted file mode 100644 index 8715d22a59a..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/LineItemSize.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.deals.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -/** - * Defines the contract for lineItems[].sizes[]. - */ -@AllArgsConstructor(staticName = "of") -@Value -public class LineItemSize { - - Integer w; - - Integer h; -} diff --git a/src/main/java/org/prebid/server/deals/proto/Price.java b/src/main/java/org/prebid/server/deals/proto/Price.java deleted file mode 100644 index 15c3c5f1963..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/Price.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.math.BigDecimal; - -@AllArgsConstructor(staticName = "of") -@Value -public class Price { - - BigDecimal cpm; - - String currency; -} diff --git a/src/main/java/org/prebid/server/deals/proto/RegisterRequest.java b/src/main/java/org/prebid/server/deals/proto/RegisterRequest.java deleted file mode 100644 index 4895468c6b7..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/RegisterRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.math.BigDecimal; - -@AllArgsConstructor(staticName = "of") -@Value -public class RegisterRequest { - - @JsonProperty("healthIndex") - BigDecimal healthIndex; - - Status status; - - @JsonProperty("hostInstanceId") - String hostInstanceId; - - String region; - - String vendor; -} diff --git a/src/main/java/org/prebid/server/deals/proto/Status.java b/src/main/java/org/prebid/server/deals/proto/Status.java deleted file mode 100644 index 974ae7fe0f2..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/Status.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; -import org.prebid.server.deals.proto.report.DeliveryProgressReport; - -@AllArgsConstructor(staticName = "of") -@Value -public class Status { - - @JsonProperty("currencyRates") - CurrencyServiceState currencyRates; - - @JsonProperty("dealsStatus") - DeliveryProgressReport deliveryProgressReport; - -} diff --git a/src/main/java/org/prebid/server/deals/proto/Token.java b/src/main/java/org/prebid/server/deals/proto/Token.java deleted file mode 100644 index ab004798820..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/Token.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.deals.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -/** - * Defines the contract for lineItems[].deliverySchedule[].tokens[]. - */ -@Value(staticConstructor = "of") -public class Token { - - @JsonProperty("class") - Integer priorityClass; - - Integer total; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReport.java b/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReport.java deleted file mode 100644 index b1a133b2299..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReport.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.Set; - -@Builder(toBuilder = true) -@Value -public class DeliveryProgressReport { - - @JsonProperty("reportId") - String reportId; - - @JsonProperty("reportTimeStamp") - String reportTimeStamp; - - @JsonProperty("dataWindowStartTimeStamp") - String dataWindowStartTimeStamp; - - @JsonProperty("dataWindowEndTimeStamp") - String dataWindowEndTimeStamp; - - @JsonProperty("instanceId") - String instanceId; - - String vendor; - - String region; - - @JsonProperty("clientAuctions") - Long clientAuctions; - - @JsonProperty("lineItemStatus") - Set lineItemStatus; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReportBatch.java b/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReportBatch.java deleted file mode 100644 index 91226ef7d32..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReportBatch.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.Set; - -@Value -@AllArgsConstructor(staticName = "of") -public class DeliveryProgressReportBatch { - - Set reports; - - String reportId; - - String dataWindowEndTimeStamp; - - public void removeReports(Set reports) { - this.reports.removeAll(reports); - } -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/DeliverySchedule.java b/src/main/java/org/prebid/server/deals/proto/report/DeliverySchedule.java deleted file mode 100644 index de1e2df8427..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/DeliverySchedule.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.Set; - -@Builder(toBuilder = true) -@Value -public class DeliverySchedule { - - @JsonProperty("planId") - String planId; - - @JsonProperty("planStartTimeStamp") - String planStartTimeStamp; - - @JsonProperty("planExpirationTimeStamp") - String planExpirationTimeStamp; - - @JsonProperty("planUpdatedTimeStamp") - String planUpdatedTimeStamp; - - Set tokens; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/Event.java b/src/main/java/org/prebid/server/deals/proto/report/Event.java deleted file mode 100644 index 686f84b0e96..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/Event.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.concurrent.atomic.LongAdder; - -@AllArgsConstructor(staticName = "of") -@Value -public class Event { - - String type; - - LongAdder count; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/LineItemStatus.java b/src/main/java/org/prebid/server/deals/proto/report/LineItemStatus.java deleted file mode 100644 index cb5f1629848..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/LineItemStatus.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.Set; - -@Builder -@Value -public class LineItemStatus { - - @JsonProperty("lineItemSource") - String lineItemSource; - - @JsonProperty("lineItemId") - String lineItemId; - - @JsonProperty("dealId") - String dealId; - - @JsonProperty("extLineItemId") - String extLineItemId; - - @JsonProperty("accountAuctions") - Long accountAuctions; - - @JsonProperty("domainMatched") - Long domainMatched; - - @JsonProperty("targetMatched") - Long targetMatched; - - @JsonProperty("targetMatchedButFcapped") - Long targetMatchedButFcapped; - - @JsonProperty("targetMatchedButFcapLookupFailed") - Long targetMatchedButFcapLookupFailed; - - @JsonProperty("pacingDeferred") - Long pacingDeferred; - - @JsonProperty("sentToBidder") - Long sentToBidder; - - @JsonProperty("sentToBidderAsTopMatch") - Long sentToBidderAsTopMatch; - - @JsonProperty("receivedFromBidder") - Long receivedFromBidder; - - @JsonProperty("receivedFromBidderInvalidated") - Long receivedFromBidderInvalidated; - - @JsonProperty("sentToClient") - Long sentToClient; - - @JsonProperty("sentToClientAsTopMatch") - Long sentToClientAsTopMatch; - - @JsonProperty("lostToLineItems") - Set lostToLineItems; - - Set events; - - @JsonProperty("deliverySchedule") - Set deliverySchedule; - - @JsonProperty("readyAt") - String readyAt; - - @JsonProperty("spentTokens") - Long spentTokens; - - @JsonProperty("pacingFrequency") - Long pacingFrequency; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/LineItemStatusReport.java b/src/main/java/org/prebid/server/deals/proto/report/LineItemStatusReport.java deleted file mode 100644 index d301993e6d1..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/LineItemStatusReport.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Builder; -import lombok.Value; - -import java.time.ZonedDateTime; - -@Builder -@Value -public class LineItemStatusReport { - - @JsonProperty("lineItemId") - String lineItemId; - - @JsonProperty("deliverySchedule") - DeliverySchedule deliverySchedule; - - @JsonProperty("spentTokens") - Long spentTokens; - - @JsonProperty("readyToServeTimestamp") - ZonedDateTime readyToServeTimestamp; - - @JsonProperty("pacingFrequency") - Long pacingFrequency; - - @JsonProperty("accountId") - String accountId; - - ObjectNode target; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/LostToLineItem.java b/src/main/java/org/prebid/server/deals/proto/report/LostToLineItem.java deleted file mode 100644 index 137f3cc8f4a..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/LostToLineItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class LostToLineItem { - - @JsonProperty("lineItemSource") - String lineItemSource; - - @JsonProperty("lineItemId") - String lineItemId; - - Long count; -} diff --git a/src/main/java/org/prebid/server/deals/proto/report/Token.java b/src/main/java/org/prebid/server/deals/proto/report/Token.java deleted file mode 100644 index 7e17ece8b8e..00000000000 --- a/src/main/java/org/prebid/server/deals/proto/report/Token.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.deals.proto.report; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -@Value(staticConstructor = "of") -public class Token { - - @JsonProperty("class") - Integer priorityClass; - - Integer total; - - Long spent; - - @JsonProperty("totalSpent") - Long totalSpent; -} diff --git a/src/main/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandler.java b/src/main/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandler.java deleted file mode 100644 index 18444679938..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandler.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.prebid.server.deals.simulation; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.core.MultiMap; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.HttpUtil; - -import java.time.ZonedDateTime; -import java.util.Map; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class DealsSimulationAdminHandler implements Handler { - - private static final TypeReference> BID_RATES_TYPE_REFERENCE = - new TypeReference<>() { - }; - - private static final Logger logger = LoggerFactory.getLogger(DealsSimulationAdminHandler.class); - - private static final Pattern URL_SUFFIX_PATTERN = Pattern.compile("/pbs-admin/e2eAdmin(.*)"); - private static final String PLANNER_REGISTER_PATH = "/planner/register"; - private static final String PLANNER_FETCH_PATH = "/planner/fetchLineItems"; - private static final String ADVANCE_PLAN_PATH = "/advancePlans"; - private static final String REPORT_PATH = "/dealstats/report"; - private static final String BID_RATE_PATH = "/bidRate"; - private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; - - private final SimulationAwareRegisterService registerService; - private final SimulationAwarePlannerService plannerService; - private final SimulationAwareDeliveryProgressService deliveryProgressService; - private final SimulationAwareDeliveryStatsService deliveryStatsService; - private final SimulationAwareHttpBidderRequester httpBidderRequester; - private final JacksonMapper mapper; - private final String endpoint; - - public DealsSimulationAdminHandler( - SimulationAwareRegisterService registerService, - SimulationAwarePlannerService plannerService, - SimulationAwareDeliveryProgressService deliveryProgressService, - SimulationAwareDeliveryStatsService deliveryStatsService, - SimulationAwareHttpBidderRequester httpBidderRequester, - JacksonMapper mapper, - String endpoint) { - - this.registerService = Objects.requireNonNull(registerService); - this.plannerService = Objects.requireNonNull(plannerService); - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.deliveryStatsService = Objects.requireNonNull(deliveryStatsService); - this.httpBidderRequester = httpBidderRequester; - this.mapper = Objects.requireNonNull(mapper); - this.endpoint = Objects.requireNonNull(endpoint); - } - - @Override - public void handle(RoutingContext routingContext) { - final HttpServerRequest request = routingContext.request(); - final Matcher matcher = URL_SUFFIX_PATTERN.matcher(request.uri()); - - if (!matcher.find() || StringUtils.isBlank(matcher.group(1))) { - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.NOT_FOUND.code()) - .end("Requested url was not found")); - return; - } - - try { - final String endpointPath = matcher.group(1); - final ZonedDateTime now = getPgSimDate(endpointPath, request.headers()); - handleEndpoint(routingContext, endpointPath, now); - - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.OK.code()) - .end()); - } catch (InvalidRequestException e) { - logger.error(e.getMessage(), e); - respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, e.getMessage()); - } catch (NotFoundException e) { - logger.error(e.getMessage(), e); - respondWith(routingContext, HttpResponseStatus.NOT_FOUND, e.getMessage()); - } catch (Exception e) { - logger.error(e.getMessage(), e); - respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } - - private ZonedDateTime getPgSimDate(String endpointPath, MultiMap headers) { - ZonedDateTime now = null; - if (!endpointPath.equals(BID_RATE_PATH)) { - now = HttpUtil.getDateFromHeader(headers, PG_SIM_TIMESTAMP); - if (now == null) { - throw new InvalidRequestException( - "pg-sim-timestamp with simulated current date is required for endpoints: %s, %s, %s, %s" - .formatted(PLANNER_REGISTER_PATH, PLANNER_FETCH_PATH, ADVANCE_PLAN_PATH, REPORT_PATH)); - } - } - return now; - } - - private void handleEndpoint(RoutingContext routingContext, String endpointPath, ZonedDateTime now) { - if (endpointPath.startsWith(PLANNER_REGISTER_PATH)) { - registerService.performRegistration(now); - - } else if (endpointPath.startsWith(PLANNER_FETCH_PATH)) { - plannerService.initiateLineItemsFetching(now); - - } else if (endpointPath.startsWith(ADVANCE_PLAN_PATH)) { - plannerService.advancePlans(now); - - } else if (endpointPath.startsWith(REPORT_PATH)) { - deliveryProgressService.createDeliveryProgressReport(now); - deliveryStatsService.sendDeliveryProgressReports(now); - - } else if (endpointPath.startsWith(BID_RATE_PATH)) { - if (httpBidderRequester != null) { - handleBidRatesEndpoint(routingContext); - } else { - throw new InvalidRequestException(""" - Calling %s is not make sense since Prebid Server configured \ - to use real bidder exchanges in simulation mode""".formatted(BID_RATE_PATH)); - } - } else { - throw new NotFoundException("Requested url %s was not found".formatted(endpointPath)); - } - } - - private void handleBidRatesEndpoint(RoutingContext routingContext) { - final Buffer body = routingContext.getBody(); - if (body == null) { - throw new InvalidRequestException("Body is required for %s endpoint".formatted(BID_RATE_PATH)); - } - - try { - httpBidderRequester.setBidRates(mapper.decodeValue(body, BID_RATES_TYPE_REFERENCE)); - } catch (DecodeException e) { - throw new InvalidRequestException("Failed to parse bid rates body: " + e.getMessage()); - } - } - - private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(status.code()) - .end(body)); - } - - private static class NotFoundException extends RuntimeException { - NotFoundException(String message) { - super(message); - } - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryProgressService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryProgressService.java deleted file mode 100644 index 511c37207fb..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryProgressService.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.prebid.server.deals.simulation; - -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.deals.DeliveryProgressReportFactory; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.DeliveryStatsService; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.deals.model.DeliveryProgressProperties; -import org.prebid.server.log.CriteriaLogManager; -import org.prebid.server.util.HttpUtil; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.util.Map; - -public class SimulationAwareDeliveryProgressService extends DeliveryProgressService { - - private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; - - private final long readyAtAdjustment; - private volatile boolean firstReportUpdate; - - public SimulationAwareDeliveryProgressService(DeliveryProgressProperties deliveryProgressProperties, - LineItemService lineItemService, - DeliveryStatsService deliveryStatsService, - DeliveryProgressReportFactory deliveryProgressReportFactory, - long readyAtAdjustment, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - super( - deliveryProgressProperties, - lineItemService, - deliveryStatsService, - deliveryProgressReportFactory, - clock, - criteriaLogManager); - this.readyAtAdjustment = readyAtAdjustment; - this.firstReportUpdate = true; - } - - @Override - public void shutdown() { - // disable sending report during bean destroying process - } - - @Override - public void processAuctionEvent(AuctionContext auctionContext) { - final ZonedDateTime now = HttpUtil.getDateFromHeader(auctionContext.getHttpRequest().getHeaders(), - PG_SIM_TIMESTAMP); - if (firstReportUpdate) { - firstReportUpdate = false; - updateDeliveryProgressesStartTime(now); - } - super.processAuctionEvent(auctionContext.getTxnLog(), auctionContext.getAccount().getId(), now); - } - - protected void incrementTokens(LineItem lineItem, ZonedDateTime now, Map planIdToTokenPriority) { - final Integer classPriority = lineItem.incSpentToken(now, readyAtAdjustment); - if (classPriority != null) { - planIdToTokenPriority.put(lineItem.getActiveDeliveryPlan().getPlanId(), classPriority); - } - } - - private void updateDeliveryProgressesStartTime(ZonedDateTime now) { - overallDeliveryProgress.setStartTimeStamp(now); - currentDeliveryProgress.setStartTimeStamp(now); - } - - void createDeliveryProgressReport(ZonedDateTime now) { - createDeliveryProgressReports(now); - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryStatsService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryStatsService.java deleted file mode 100644 index 7debdb649ed..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryStatsService.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.prebid.server.deals.simulation; - -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.Vertx; -import org.prebid.server.deals.AlertHttpService; -import org.prebid.server.deals.DeliveryProgressReportFactory; -import org.prebid.server.deals.DeliveryStatsService; -import org.prebid.server.deals.model.DeliveryStatsProperties; -import org.prebid.server.deals.proto.report.DeliveryProgressReport; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.http.HttpClient; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; - -public class SimulationAwareDeliveryStatsService extends DeliveryStatsService { - - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - - private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; - - public SimulationAwareDeliveryStatsService(DeliveryStatsProperties deliveryStatsProperties, - DeliveryProgressReportFactory deliveryProgressReportFactory, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - Vertx vertx, - JacksonMapper mapper) { - super(deliveryStatsProperties, - deliveryProgressReportFactory, - alertHttpService, - httpClient, - metrics, - clock, - vertx, - mapper); - } - - @Override - protected Future sendReport(DeliveryProgressReport deliveryProgressReport, MultiMap headers, - ZonedDateTime now) { - return super.sendReport(deliveryProgressReport, - headers().add(PG_SIM_TIMESTAMP, UTC_MILLIS_FORMATTER.format(now)), now); - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequester.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequester.java deleted file mode 100644 index 8c201ac081f..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequester.java +++ /dev/null @@ -1,177 +0,0 @@ -package org.prebid.server.deals.simulation; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.iab.openrtb.request.Deal; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import io.vertx.core.Future; -import lombok.AllArgsConstructor; -import lombok.Value; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.auction.model.BidRejectionReason; -import org.prebid.server.auction.model.BidRejectionTracker; -import org.prebid.server.auction.model.BidderRequest; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.BidderErrorNotifier; -import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory; -import org.prebid.server.bidder.HttpBidderRequestEnricher; -import org.prebid.server.bidder.HttpBidderRequester; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.lineitem.LineItem; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.model.CaseInsensitiveMultiMap; -import org.prebid.server.proto.openrtb.ext.request.ExtDeal; -import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.vertx.http.HttpClient; - -import java.math.BigDecimal; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class SimulationAwareHttpBidderRequester extends HttpBidderRequester { - - private static final BigDecimal DEFAULT_CPM = BigDecimal.ONE; - private static final String DEFAULT_ADM = ""; - private static final String DEFAULT_CRID = "crid"; - private static final String DEFAULT_CURRENCY = "USD"; - private static final String BID_ID_FORMAT = "%s-%s"; - - private final Map bidRates; - private final LineItemService lineItemService; - private final JacksonMapper mapper; - - public SimulationAwareHttpBidderRequester( - HttpClient httpClient, - BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory, - BidderErrorNotifier bidderErrorNotifier, - HttpBidderRequestEnricher requestEnricher, - LineItemService lineItemService, - JacksonMapper mapper) { - - super(httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier, requestEnricher, mapper); - - this.lineItemService = Objects.requireNonNull(lineItemService); - this.mapper = Objects.requireNonNull(mapper); - this.bidRates = new HashMap<>(); - } - - void setBidRates(Map bidRates) { - this.bidRates.putAll(bidRates); - } - - @Override - public Future requestBids(Bidder bidder, - BidderRequest bidderRequest, - BidRejectionTracker bidRejectionTracker, - Timeout timeout, - CaseInsensitiveMultiMap requestHeaders, - BidderAliases aliases, - boolean debugEnabled) { - - final List imps = bidderRequest.getBidRequest().getImp(); - final Map idToImps = imps.stream().collect(Collectors.toMap(Imp::getId, Function.identity())); - final Map> impsToDealInfo = imps.stream() - .filter(imp -> imp.getPmp() != null) - .collect(Collectors.toMap(Imp::getId, imp -> imp.getPmp().getDeals().stream() - .map(deal -> DealInfo.of(deal.getId(), getLineItemId(deal))) - .filter(dealInfo -> dealInfo.getLineItemId() != null) - .collect(Collectors.toSet()))); - - if (impsToDealInfo.values().stream().noneMatch(CollectionUtils::isNotEmpty)) { - bidRejectionTracker.rejectAll(BidRejectionReason.FAILED_TO_REQUEST_BIDS); - - return Future.succeededFuture(BidderSeatBid.builder() - .errors(Collections.singletonList(BidderError.failedToRequestBids( - "Matched or ready to serve line items were not found, but required in simulation mode"))) - .build()); - } - - final List bidderBids = impsToDealInfo.entrySet().stream() - .flatMap(impToDealInfo -> impToDealInfo.getValue() - .stream() - .map(dealInfo -> createBid(idToImps.get(impToDealInfo.getKey()), dealInfo.getDealId(), - dealInfo.getLineItemId())) - .filter(Objects::nonNull)) - .map(bid -> BidderBid.of(bid, BidType.banner, DEFAULT_CURRENCY)) - .toList(); - - return Future.succeededFuture(BidderSeatBid.of(bidderBids)); - } - - private String getLineItemId(Deal deal) { - final JsonNode extDealNode = deal.getExt(); - final ExtDeal extDeal = extDealNode != null ? getExtDeal(extDealNode) : null; - final ExtDealLine extDealLine = extDeal != null ? extDeal.getLine() : null; - return extDealLine != null ? extDealLine.getLineItemId() : null; - } - - private Bid createBid(Imp imp, String dealId, String lineItemId) { - final Double rate = bidRates.get(lineItemId); - if (rate == null) { - throw new PreBidException("Bid rate for line item with id %s was not found".formatted(lineItemId)); - } - final String impId = imp.getId(); - final LineItem lineItem = lineItemService.getLineItemById(lineItemId); - final List sizes = getLineItemSizes(imp); - return Math.random() < rate - ? Bid.builder() - .id(BID_ID_FORMAT.formatted(impId, lineItemId)) - .impid(impId) - .dealid(dealId) - .price(lineItem != null ? lineItem.getCpm() : DEFAULT_CPM) - .adm(DEFAULT_ADM) - .crid(DEFAULT_CRID) - .w(sizes.isEmpty() ? 0 : sizes.get(0).getW()) - .h(sizes.isEmpty() ? 0 : sizes.get(0).getH()) - .build() - : null; - } - - private List getLineItemSizes(Imp imp) { - return imp.getPmp().getDeals().stream() - .map(Deal::getExt) - .filter(Objects::nonNull) - .map(this::getExtDeal) - .filter(Objects::nonNull) - .map(ExtDeal::getLine) - .filter(Objects::nonNull) - .map(ExtDealLine::getSizes) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .toList(); - } - - private ExtDeal getExtDeal(JsonNode extDeal) { - try { - return mapper.mapper().treeToValue(extDeal, ExtDeal.class); - } catch (JsonProcessingException e) { - throw new PreBidException("Error decoding bidRequest.imp.pmp.deal.ext: " + e.getMessage(), e); - } - } - - @Value - @AllArgsConstructor(staticName = "of") - private static class DealInfo { - - String dealId; - - String lineItemId; - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareLineItemService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareLineItemService.java deleted file mode 100644 index 1751ef8abd5..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareLineItemService.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.prebid.server.deals.simulation; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.TargetingService; -import org.prebid.server.deals.events.ApplicationEventService; -import org.prebid.server.deals.model.MatchLineItemsResult; -import org.prebid.server.log.CriteriaLogManager; -import org.prebid.server.util.HttpUtil; -import org.springframework.beans.factory.annotation.Value; - -import java.time.Clock; - -public class SimulationAwareLineItemService extends LineItemService { - - private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; - - public SimulationAwareLineItemService(int maxDealsPerBidder, - TargetingService targetingService, - CurrencyConversionService conversionService, - ApplicationEventService applicationEventService, - @Value("${auction.ad-server-currency}}") String adServerCurrency, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - super( - maxDealsPerBidder, - targetingService, - conversionService, - applicationEventService, - adServerCurrency, - clock, - criteriaLogManager); - } - - @Override - public boolean accountHasDeals(AuctionContext auctionContext) { - return accountHasDeals( - auctionContext.getAccount().getId(), - HttpUtil.getDateFromHeader(auctionContext.getHttpRequest().getHeaders(), PG_SIM_TIMESTAMP)); - } - - @Override - public MatchLineItemsResult findMatchingLineItems(BidRequest bidRequest, - Imp imp, - String bidder, - BidderAliases aliases, - AuctionContext auctionContext) { - - return findMatchingLineItems( - bidRequest, - imp, - bidder, - aliases, - auctionContext, - HttpUtil.getDateFromHeader(auctionContext.getHttpRequest().getHeaders(), PG_SIM_TIMESTAMP)); - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwarePlannerService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwarePlannerService.java deleted file mode 100644 index 2009f94e63f..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwarePlannerService.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.prebid.server.deals.simulation; - -import io.vertx.core.AsyncResult; -import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.deals.AlertHttpService; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.PlannerService; -import org.prebid.server.deals.model.AlertPriority; -import org.prebid.server.deals.model.DeploymentProperties; -import org.prebid.server.deals.model.PlannerProperties; -import org.prebid.server.deals.proto.LineItemMetaData; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.http.HttpClient; - -import java.time.Clock; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -public class SimulationAwarePlannerService extends PlannerService { - - private static final Logger logger = LoggerFactory.getLogger(SimulationAwarePlannerService.class); - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - - private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; - private static final String PBS_PLANNER_CLIENT_ERROR = "pbs-planner-client-error"; - - private final SimulationAwareLineItemService lineItemService; - private final Metrics metrics; - private final AlertHttpService alertHttpService; - - private List lineItemMetaData; - - public SimulationAwarePlannerService(PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - SimulationAwareLineItemService lineItemService, - DeliveryProgressService deliveryProgressService, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - JacksonMapper mapper) { - super( - plannerProperties, - deploymentProperties, - lineItemService, - deliveryProgressService, - alertHttpService, - httpClient, - metrics, - clock, - mapper); - - this.lineItemService = Objects.requireNonNull(lineItemService); - this.alertHttpService = Objects.requireNonNull(alertHttpService); - this.metrics = Objects.requireNonNull(metrics); - this.lineItemMetaData = new ArrayList<>(); - } - - public void advancePlans(ZonedDateTime now) { - lineItemService.updateLineItems(lineItemMetaData, isPlannerResponsive.get(), now); - lineItemService.advanceToNextPlan(now); - } - - public void initiateLineItemsFetching(ZonedDateTime now) { - fetchLineItemMetaData(planEndpoint, headers(now)) - .onComplete(this::handleInitializationResult); - } - - /** - * Handles result of initialization process. Sets metadata if request was successful. - */ - @Override - protected void handleInitializationResult(AsyncResult> plannerResponse) { - if (plannerResponse.succeeded()) { - metrics.updatePlannerRequestMetric(true); - isPlannerResponsive.set(true); - lineItemService.updateIsPlannerResponsive(true); - lineItemMetaData = plannerResponse.result(); - } else { - alert(plannerResponse.cause().getMessage(), AlertPriority.HIGH, logger::warn); - logger.warn("Failed to retrieve line items from Planner after retry. Reason: {0}", - plannerResponse.cause().getMessage()); - isPlannerResponsive.set(false); - lineItemService.updateIsPlannerResponsive(false); - metrics.updatePlannerRequestMetric(false); - } - } - - private MultiMap headers(ZonedDateTime now) { - return headers().add(PG_SIM_TIMESTAMP, UTC_MILLIS_FORMATTER.format(now)); - } - - private void alert(String message, AlertPriority alertPriority, Consumer logger) { - alertHttpService.alert(PBS_PLANNER_CLIENT_ERROR, alertPriority, message); - logger.accept(message); - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareRegisterService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareRegisterService.java deleted file mode 100644 index d185285937e..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareRegisterService.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.prebid.server.deals.simulation; - -import io.vertx.core.MultiMap; -import io.vertx.core.Vertx; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.AlertHttpService; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.RegisterService; -import org.prebid.server.deals.events.AdminEventService; -import org.prebid.server.deals.model.DeploymentProperties; -import org.prebid.server.deals.model.PlannerProperties; -import org.prebid.server.health.HealthMonitor; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.vertx.http.HttpClient; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; - -public class SimulationAwareRegisterService extends RegisterService { - - private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .toFormatter(); - private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; - - public SimulationAwareRegisterService(PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - AdminEventService adminEventService, - DeliveryProgressService deliveryProgressService, - AlertHttpService alertHttpService, - HealthMonitor healthMonitor, - CurrencyConversionService currencyConversionService, - HttpClient httpClient, - Vertx vertx, - JacksonMapper mapper) { - super(plannerProperties, - deploymentProperties, - adminEventService, - deliveryProgressService, - alertHttpService, - healthMonitor, - currencyConversionService, - httpClient, - vertx, - mapper); - } - - @Override - public void initialize() { - // disable timer initialization for simulation mode - } - - public void performRegistration(ZonedDateTime now) { - register(headers(now)); - } - - private MultiMap headers(ZonedDateTime now) { - return headers().add(PG_SIM_TIMESTAMP, UTC_MILLIS_FORMATTER.format(now)); - } -} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareUserService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareUserService.java deleted file mode 100644 index 78d9f8ae409..00000000000 --- a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareUserService.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.prebid.server.deals.simulation; - -import io.vertx.core.Future; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.UserService; -import org.prebid.server.deals.model.SimulationProperties; -import org.prebid.server.deals.model.UserDetails; -import org.prebid.server.deals.model.UserDetailsProperties; -import org.prebid.server.execution.Timeout; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.http.HttpClient; - -import java.time.Clock; - -public class SimulationAwareUserService extends UserService { - - private final boolean winEventsEnabled; - private final boolean userDetailsEnabled; - - public SimulationAwareUserService(UserDetailsProperties userDetailsProperties, - SimulationProperties simulationProperties, - String dataCenterRegion, - LineItemService lineItemService, - HttpClient httpClient, - Clock clock, - Metrics metrics, - JacksonMapper mapper) { - super( - userDetailsProperties, - dataCenterRegion, - lineItemService, - httpClient, - clock, - metrics, - mapper); - - this.winEventsEnabled = simulationProperties.isWinEventsEnabled(); - this.userDetailsEnabled = simulationProperties.isUserDetailsEnabled(); - } - - @Override - public Future getUserDetails(AuctionContext context, Timeout timeout) { - return userDetailsEnabled - ? super.getUserDetails(context, timeout) - : Future.succeededFuture(UserDetails.empty()); - } - - @Override - public void processWinEvent(String lineItemId, String bidId, UidsCookie uids) { - if (winEventsEnabled) { - super.processWinEvent(lineItemId, bidId, uids); - } - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/RequestContext.java b/src/main/java/org/prebid/server/deals/targeting/RequestContext.java deleted file mode 100644 index 8aef7c152c0..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/RequestContext.java +++ /dev/null @@ -1,422 +0,0 @@ -package org.prebid.server.deals.targeting; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Data; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Geo; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Segment; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.prebid.server.deals.model.TxnLog; -import org.prebid.server.deals.targeting.model.GeoLocation; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.model.Size; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; -import org.prebid.server.exception.TargetingSyntaxException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.FlexibleExtension; -import org.prebid.server.proto.openrtb.ext.request.ExtApp; -import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserTime; -import org.prebid.server.util.StreamUtil; - -import java.beans.BeanInfo; -import java.beans.FeatureDescriptor; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class RequestContext { - - private static final String EXT_BIDDER = "bidder."; - private static final String EXT_CONTEXT_DATA = "context.data."; - private static final String EXT_DATA = "data."; - - private final BidRequest bidRequest; - private final Imp imp; - private final TxnLog txnLog; - - private final AttributeReader impReader; - private final AttributeReader geoReader; - private final AttributeReader deviceReader; - private final AttributeReader userReader; - private final AttributeReader siteReader; - private final AttributeReader appReader; - - public RequestContext(BidRequest bidRequest, - Imp imp, - TxnLog txnLog, - JacksonMapper mapper) { - - this.bidRequest = Objects.requireNonNull(bidRequest); - this.imp = Objects.requireNonNull(imp); - this.txnLog = Objects.requireNonNull(txnLog); - - impReader = AttributeReader.forImp(); - geoReader = AttributeReader.forGeo(getExtNode( - bidRequest.getDevice(), - device -> getIfNotNull(getIfNotNull(device, Device::getGeo), Geo::getExt), - mapper)); - deviceReader = AttributeReader.forDevice(getExtNode(bidRequest.getDevice(), Device::getExt, mapper)); - userReader = AttributeReader.forUser(); - siteReader = AttributeReader.forSite(); - appReader = AttributeReader.forApp(); - } - - private static ObjectNode getExtNode(T target, - Function extExtractor, - JacksonMapper mapper) { - - final FlexibleExtension ext = target != null ? extExtractor.apply(target) : null; - return ext != null ? (ObjectNode) mapper.mapper().valueToTree(ext) : null; - } - - public LookupResult lookupString(TargetingCategory category) { - final TargetingCategory.Type type = category.type(); - final String path = category.path(); - - return switch (type) { - case domain -> lookupResult( - getIfNotNull(bidRequest.getSite(), Site::getDomain), - getIfNotNull(getIfNotNull(bidRequest.getSite(), Site::getPublisher), Publisher::getDomain)); - case publisherDomain -> lookupResult(getIfNotNull( - getIfNotNull(bidRequest.getSite(), Site::getPublisher), Publisher::getDomain)); - case referrer -> lookupResult(getIfNotNull(bidRequest.getSite(), Site::getPage)); - case appBundle -> lookupResult(getIfNotNull(bidRequest.getApp(), App::getBundle)); - case adslot -> lookupResult( - imp.getTagid(), - impReader.readFromExt(imp, "gpid", RequestContext::nodeToString), - impReader.readFromExt(imp, "data.pbadslot", RequestContext::nodeToString), - impReader.readFromExt(imp, "data.adserver.adslot", RequestContext::nodeToString)); - case deviceGeoExt -> lookupResult(geoReader.readFromExt( - getIfNotNull(bidRequest.getDevice(), Device::getGeo), path, RequestContext::nodeToString)); - case deviceExt -> lookupResult( - deviceReader.readFromExt(bidRequest.getDevice(), path, RequestContext::nodeToString)); - case bidderParam -> lookupResult( - impReader.readFromExt(imp, EXT_BIDDER + path, RequestContext::nodeToString)); - case userFirstPartyData -> - userReader.read(bidRequest.getUser(), path, RequestContext::nodeToString, String.class); - case siteFirstPartyData -> getSiteFirstPartyData(path, RequestContext::nodeToString); - default -> LookupResult.empty(); - }; - } - - public LookupResult lookupInteger(TargetingCategory category) { - final TargetingCategory.Type type = category.type(); - final String path = category.path(); - - return switch (type) { - case pagePosition -> lookupResult(getIfNotNull(getIfNotNull(imp, Imp::getBanner), Banner::getPos)); - case dow -> lookupResult(getIfNotNull( - getIfNotNull(getIfNotNull(bidRequest.getUser(), User::getExt), ExtUser::getTime), - ExtUserTime::getUserdow)); - case hour -> lookupResult(getIfNotNull( - getIfNotNull(getIfNotNull(bidRequest.getUser(), User::getExt), ExtUser::getTime), - ExtUserTime::getUserhour)); - case deviceGeoExt -> lookupResult(geoReader.readFromExt( - getIfNotNull(bidRequest.getDevice(), Device::getGeo), path, RequestContext::nodeToInteger)); - case bidderParam -> lookupResult( - impReader.readFromExt(imp, EXT_BIDDER + path, RequestContext::nodeToInteger)); - case userFirstPartyData -> - userReader.read(bidRequest.getUser(), path, RequestContext::nodeToInteger, Integer.class); - case siteFirstPartyData -> getSiteFirstPartyData(path, RequestContext::nodeToInteger); - default -> LookupResult.empty(); - }; - } - - public LookupResult> lookupStrings(TargetingCategory category) { - final TargetingCategory.Type type = category.type(); - final String path = category.path(); - final User user = bidRequest.getUser(); - - return switch (type) { - case mediaType -> lookupResult(getMediaTypes()); - case bidderParam -> lookupResult( - impReader.readFromExt(imp, EXT_BIDDER + path, RequestContext::nodeToListOfStrings)); - case userSegment -> lookupResult(getSegments(category)); - case userFirstPartyData -> lookupResult( - listOfNonNulls(userReader.readFromObject(user, path, String.class)), - userReader.readFromExt(user, path, RequestContext::nodeToListOfStrings)); - case siteFirstPartyData -> getSiteFirstPartyData(path, RequestContext::nodeToListOfStrings); - default -> LookupResult.empty(); - }; - } - - public LookupResult> lookupIntegers(TargetingCategory category) { - final TargetingCategory.Type type = category.type(); - final String path = category.path(); - final User user = bidRequest.getUser(); - - return switch (type) { - case bidderParam -> lookupResult( - impReader.readFromExt(imp, EXT_BIDDER + path, RequestContext::nodeToListOfIntegers)); - case userFirstPartyData -> lookupResult( - listOfNonNulls(userReader.readFromObject(user, path, Integer.class)), - userReader.readFromExt(user, path, RequestContext::nodeToListOfIntegers)); - case siteFirstPartyData -> getSiteFirstPartyData(path, RequestContext::nodeToListOfIntegers); - default -> LookupResult.empty(); - }; - } - - public LookupResult> lookupSizes(TargetingCategory category) { - final TargetingCategory.Type type = category.type(); - if (type != TargetingCategory.Type.size) { - throw new TargetingSyntaxException("Unexpected category for fetching sizes for: " + type); - } - - final List sizes = ListUtils.union(sizesFromBanner(imp), sizesFromVideo(imp)); - - return !sizes.isEmpty() ? LookupResult.ofValue(sizes) : LookupResult.empty(); - } - - private static List sizesFromBanner(Imp imp) { - final List formats = getIfNotNull(imp.getBanner(), Banner::getFormat); - return ListUtils.emptyIfNull(formats).stream() - .map(format -> Size.of(format.getW(), format.getH())) - .toList(); - } - - private static List sizesFromVideo(Imp imp) { - final Video video = imp.getVideo(); - final Integer width = video != null ? video.getW() : null; - final Integer height = video != null ? video.getH() : null; - - return width != null && height != null - ? Collections.singletonList(Size.of(width, height)) - : Collections.emptyList(); - } - - public GeoLocation lookupGeoLocation(TargetingCategory category) { - final TargetingCategory.Type type = category.type(); - if (type != TargetingCategory.Type.location) { - throw new TargetingSyntaxException("Unexpected category for fetching geo location for: " + type); - } - - final Geo geo = getIfNotNull(getIfNotNull(bidRequest, BidRequest::getDevice), Device::getGeo); - final Float lat = getIfNotNull(geo, Geo::getLat); - final Float lon = getIfNotNull(geo, Geo::getLon); - - return lat != null && lon != null ? GeoLocation.of(lat, lon) : null; - } - - public TxnLog txnLog() { - return txnLog; - } - - @SafeVarargs - private static LookupResult lookupResult(T... candidates) { - return LookupResult.of(listOfNonNulls(candidates)); - } - - @SafeVarargs - private static List listOfNonNulls(T... candidates) { - return Stream.of(candidates) - .filter(Objects::nonNull) - .toList(); - } - - private static T getIfNotNull(S source, Function getter) { - return source != null ? getter.apply(source) : null; - } - - private List getMediaTypes() { - final List mediaTypes = new ArrayList<>(); - if (imp.getBanner() != null) { - mediaTypes.add("banner"); - } - if (imp.getVideo() != null) { - mediaTypes.add("video"); - } - if (imp.getXNative() != null) { - mediaTypes.add("native"); - } - return mediaTypes; - } - - private LookupResult getSiteFirstPartyData(String path, Function valueExtractor) { - return lookupResult( - impReader.readFromExt(imp, EXT_CONTEXT_DATA + path, valueExtractor), - impReader.readFromExt(imp, EXT_DATA + path, valueExtractor), - siteReader.readFromExt(bidRequest.getSite(), path, valueExtractor), - appReader.readFromExt(bidRequest.getApp(), path, valueExtractor)); - } - - private List getSegments(TargetingCategory category) { - final List userData = getIfNotNull(bidRequest.getUser(), User::getData); - - final List segments = ListUtils.emptyIfNull(userData) - .stream() - .filter(Objects::nonNull) - .filter(data -> Objects.equals(data.getId(), category.path())) - .flatMap(data -> ListUtils.emptyIfNull(data.getSegment()).stream()) - .map(Segment::getId) - .filter(Objects::nonNull) - .toList(); - - return !segments.isEmpty() ? segments : null; - } - - private static String toJsonPointer(String path) { - return Arrays.stream(path.split("\\.")) - .collect(Collectors.joining("/", "/", StringUtils.EMPTY)); - } - - private static String nodeToString(JsonNode node) { - return node.isTextual() ? node.asText() : null; - } - - private static Integer nodeToInteger(JsonNode node) { - return node.isInt() ? node.asInt() : null; - } - - private static List nodeToListOfStrings(JsonNode node) { - final Function valueExtractor = RequestContext::nodeToString; - return node.isTextual() - ? Collections.singletonList(valueExtractor.apply(node)) - : nodeToList(node, valueExtractor); - } - - private static List nodeToListOfIntegers(JsonNode node) { - final Function valueExtractor = RequestContext::nodeToInteger; - return node.isInt() - ? Collections.singletonList(valueExtractor.apply(node)) - : nodeToList(node, valueExtractor); - } - - private static List nodeToList(JsonNode node, Function valueExtractor) { - if (!node.isArray()) { - return null; - } - - return StreamUtil.asStream(node.spliterator()) - .map(valueExtractor) - .filter(Objects::nonNull) - .toList(); - } - - private static class AttributeReader { - - private static final Set> SUPPORTED_PROPERTY_TYPES = Set.of(String.class, Integer.class, int.class); - - private final Map properties; - private final Function extPathExtractor; - - private AttributeReader(Class type, Function extPathExtractor) { - this.properties = supportedBeanProperties(type); - this.extPathExtractor = extPathExtractor; - } - - public static AttributeReader forImp() { - return new AttributeReader<>( - Imp.class, - imp -> getIfNotNull(imp, Imp::getExt)); - } - - public static AttributeReader forGeo(ObjectNode geoExt) { - return new AttributeReader<>( - Geo.class, - ignored -> geoExt); - } - - public static AttributeReader forDevice(ObjectNode deviceExt) { - return new AttributeReader<>( - Device.class, - ignored -> deviceExt); - } - - public static AttributeReader forUser() { - return new AttributeReader<>( - User.class, - user -> getIfNotNull(getIfNotNull(user, User::getExt), ExtUser::getData)); - } - - public static AttributeReader forSite() { - return new AttributeReader<>( - Site.class, - site -> getIfNotNull(getIfNotNull(site, Site::getExt), ExtSite::getData)); - } - - public static AttributeReader forApp() { - return new AttributeReader<>( - App.class, - app -> getIfNotNull(getIfNotNull(app, App::getExt), ExtApp::getData)); - } - - public LookupResult read(T target, - String path, - Function valueExtractor, - Class attributeType) { - - return lookupResult( - // look in the object itself - readFromObject(target, path, attributeType), - // then examine ext if value not found on top level or if it is nested attribute - readFromExt(target, path, valueExtractor)); - } - - public A readFromObject(T target, String path, Class attributeType) { - return isTopLevelAttribute(path) - ? getIfNotNull(target, user -> readProperty(user, path, attributeType)) - : null; - } - - public A readFromExt(T target, String path, Function valueExtractor) { - final JsonNode extPath = getIfNotNull(target, extPathExtractor); - final JsonNode value = getIfNotNull(extPath, node -> node.at(toJsonPointer(path))); - return getIfNotNull(value, valueExtractor); - } - - private boolean isTopLevelAttribute(String path) { - return !path.contains("."); - } - - private static Map supportedBeanProperties(Class beanClass) { - try { - final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass, Object.class); - return Arrays.stream(beanInfo.getPropertyDescriptors()) - .filter(descriptor -> SUPPORTED_PROPERTY_TYPES.contains(descriptor.getPropertyType())) - .collect(Collectors.toMap(FeatureDescriptor::getName, Function.identity())); - } catch (IntrospectionException e) { - return ExceptionUtils.rethrow(e); - } - } - - @SuppressWarnings("unchecked") - private A readProperty(T target, String path, Class attributeType) { - final PropertyDescriptor descriptor = properties.get(path); - - if (descriptor != null && descriptor.getPropertyType().equals(attributeType)) { - try { - return (A) descriptor.getReadMethod().invoke(target); - } catch (IllegalAccessException | InvocationTargetException e) { - // just ignore - } - } - - return null; - } - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/TargetingDefinition.java b/src/main/java/org/prebid/server/deals/targeting/TargetingDefinition.java deleted file mode 100644 index ec1cb8683af..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/TargetingDefinition.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.deals.targeting; - -import lombok.Value; -import org.prebid.server.deals.targeting.interpret.Expression; - -@Value(staticConstructor = "of") -public class TargetingDefinition { - - Expression rootExpression; -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/And.java b/src/main/java/org/prebid/server/deals/targeting/interpret/And.java deleted file mode 100644 index bfe69b2882a..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/And.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; - -import java.util.Collections; -import java.util.List; - -@EqualsAndHashCode -public class And implements NonTerminalExpression { - - private final List expressions; - - public And(List expressions) { - this.expressions = Collections.unmodifiableList(expressions); - } - - @Override - public boolean matches(RequestContext context) { - for (final Expression expression : expressions) { - if (!expression.matches(context)) { - return false; - } - } - return true; - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/DomainMetricAwareExpression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/DomainMetricAwareExpression.java deleted file mode 100644 index 9d682256384..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/DomainMetricAwareExpression.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; - -@EqualsAndHashCode -public class DomainMetricAwareExpression implements Expression { - - private final Expression domainFunction; - private final String lineItemId; - - public DomainMetricAwareExpression(Expression domainFunction, String lineItemId) { - this.domainFunction = domainFunction; - this.lineItemId = lineItemId; - } - - @Override - public boolean matches(RequestContext requestContext) { - final boolean matches = domainFunction.matches(requestContext); - if (matches) { - requestContext.txnLog().lineItemsMatchedDomainTargeting().add(lineItemId); - } - return matches; - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Expression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Expression.java deleted file mode 100644 index 332e6a2a2eb..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/Expression.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import org.prebid.server.deals.targeting.RequestContext; - -public interface Expression { - - boolean matches(RequestContext context); -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/In.java b/src/main/java/org/prebid/server/deals/targeting/interpret/In.java deleted file mode 100644 index 299a74e254f..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/In.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -@EqualsAndHashCode -public abstract class In implements TerminalExpression { - - protected final TargetingCategory category; - - protected List values; - - public In(TargetingCategory category, List values) { - this.category = Objects.requireNonNull(category); - this.values = Collections.unmodifiableList(values); - } - - @Override - public boolean matches(RequestContext context) { - return lookupActualValue(context).anyMatch(values::contains); - } - - protected abstract LookupResult lookupActualValue(RequestContext context); -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/InIntegers.java b/src/main/java/org/prebid/server/deals/targeting/interpret/InIntegers.java deleted file mode 100644 index 99511bf484b..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/InIntegers.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -public class InIntegers extends In { - - public InIntegers(TargetingCategory category, List values) { - super(category, values); - } - - @Override - public LookupResult lookupActualValue(RequestContext context) { - return context.lookupInteger(category); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/InStrings.java b/src/main/java/org/prebid/server/deals/targeting/interpret/InStrings.java deleted file mode 100644 index 185d2069074..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/InStrings.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Stream; - -@EqualsAndHashCode(callSuper = true) -public class InStrings extends In { - - public InStrings(TargetingCategory category, List values) { - super(category, toLowerCase(values)); - } - - @Override - public LookupResult lookupActualValue(RequestContext context) { - final List actualValue = firstNonEmpty( - () -> context.lookupString(category).getValues(), - () -> lookupIntegerAsString(context)); - - return actualValue != null - ? LookupResult.of(actualValue.stream().map(String::toLowerCase).toList()) - : LookupResult.empty(); - } - - private List lookupIntegerAsString(RequestContext context) { - final List actualValue = context.lookupInteger(category).getValues(); - return actualValue.stream().map(Object::toString).toList(); - } - - private static List toLowerCase(List values) { - return values.stream().map(String::toLowerCase).toList(); - } - - @SafeVarargs - private static List firstNonEmpty(Supplier>... suppliers) { - return Stream.of(suppliers) - .map(Supplier::get) - .filter(CollectionUtils::isNotEmpty) - .findFirst() - .orElse(null); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Intersects.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Intersects.java deleted file mode 100644 index e1113947683..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/Intersects.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -@EqualsAndHashCode -public abstract class Intersects implements TerminalExpression { - - protected final TargetingCategory category; - - protected List values; - - public Intersects(TargetingCategory category, List values) { - this.category = Objects.requireNonNull(category); - this.values = Collections.unmodifiableList(values); - } - - @Override - public boolean matches(RequestContext context) { - return lookupActualValues(context) - .anyMatch(actualValues -> !Collections.disjoint(values, actualValues)); - } - - protected abstract LookupResult> lookupActualValues(RequestContext context); -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegers.java b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegers.java deleted file mode 100644 index fd3b0f618cf..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegers.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -public class IntersectsIntegers extends Intersects { - - public IntersectsIntegers(TargetingCategory category, List values) { - super(category, values); - } - - @Override - public LookupResult> lookupActualValues(RequestContext context) { - return context.lookupIntegers(category); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsSizes.java b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsSizes.java deleted file mode 100644 index 444bfdcc356..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsSizes.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.model.Size; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -public class IntersectsSizes extends Intersects { - - public IntersectsSizes(TargetingCategory category, List values) { - super(category, values); - } - - @Override - public LookupResult> lookupActualValues(RequestContext context) { - return context.lookupSizes(category); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsStrings.java b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsStrings.java deleted file mode 100644 index dd6d9dff2d8..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsStrings.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.LookupResult; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -public class IntersectsStrings extends Intersects { - - public IntersectsStrings(TargetingCategory category, List values) { - super(category, toLowerCase(values)); - } - - @Override - public LookupResult> lookupActualValues(RequestContext context) { - return LookupResult.of( - context.lookupStrings(category).getValues().stream() - .map(IntersectsStrings::toLowerCase) - .toList()); - } - - private static List toLowerCase(List values) { - return values.stream().map(String::toLowerCase).toList(); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Matches.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Matches.java deleted file mode 100644 index 4d5394eeedf..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/Matches.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.Objects; -import java.util.function.BiFunction; - -@EqualsAndHashCode -public class Matches implements TerminalExpression { - - private static final String WILDCARD = "*"; - - private final TargetingCategory category; - - private final BiFunction method; - - private final String value; - - public Matches(TargetingCategory category, String value) { - this.category = Objects.requireNonNull(category); - this.method = resolveMethod(Objects.requireNonNull(value)); - this.value = value.replaceAll("\\*", "").toLowerCase(); - } - - @Override - public boolean matches(RequestContext context) { - return context.lookupString(category) - .anyMatch(valueToMatch -> method.apply(valueToMatch.toLowerCase(), value)); - } - - private static BiFunction resolveMethod(String value) { - if (value.startsWith(WILDCARD) && value.endsWith(WILDCARD)) { - return String::contains; - } else if (value.startsWith(WILDCARD)) { - return String::endsWith; - } else if (value.endsWith(WILDCARD)) { - return String::startsWith; - } else { - return String::equals; - } - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/NonTerminalExpression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/NonTerminalExpression.java deleted file mode 100644 index ee9f4235a3d..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/NonTerminalExpression.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -public interface NonTerminalExpression extends Expression { -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Not.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Not.java deleted file mode 100644 index ed8f1a35875..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/Not.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; - -import java.util.Objects; - -@EqualsAndHashCode -public class Not implements NonTerminalExpression { - - private final Expression expression; - - public Not(Expression expression) { - this.expression = Objects.requireNonNull(expression); - } - - @Override - public boolean matches(RequestContext context) { - return !expression.matches(context); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Or.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Or.java deleted file mode 100644 index a4740f889ad..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/Or.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; - -import java.util.Collections; -import java.util.List; - -@EqualsAndHashCode -public class Or implements NonTerminalExpression { - - private final List expressions; - - public Or(List expressions) { - this.expressions = Collections.unmodifiableList(expressions); - } - - @Override - public boolean matches(RequestContext context) { - for (final Expression expression : expressions) { - if (expression.matches(context)) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/TerminalExpression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/TerminalExpression.java deleted file mode 100644 index 7c89885cc49..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/TerminalExpression.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -public interface TerminalExpression extends Expression { -} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Within.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Within.java deleted file mode 100644 index 7a4da8f30c6..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/interpret/Within.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.prebid.server.deals.targeting.interpret; - -import lombok.EqualsAndHashCode; -import org.prebid.server.deals.targeting.RequestContext; -import org.prebid.server.deals.targeting.model.GeoLocation; -import org.prebid.server.deals.targeting.model.GeoRegion; -import org.prebid.server.deals.targeting.syntax.TargetingCategory; - -import java.util.Objects; - -@EqualsAndHashCode -public class Within implements TerminalExpression { - - private static final int EARTH_RADIUS_MI = 3959; - - private final TargetingCategory category; - - private final GeoRegion value; - - public Within(TargetingCategory category, GeoRegion value) { - this.category = Objects.requireNonNull(category); - this.value = Objects.requireNonNull(value); - } - - @Override - public boolean matches(RequestContext context) { - final GeoLocation location = context.lookupGeoLocation(category); - - return location != null && isLocationWithinRegion(location); - } - - private boolean isLocationWithinRegion(GeoLocation location) { - final double distance = calculateDistance(location.getLat(), location.getLon(), value.getLat(), value.getLon()); - - return value.getRadiusMiles() > distance; - } - - private static double calculateDistance(double startLat, double startLong, double endLat, double endLong) { - final double dLat = Math.toRadians(endLat - startLat); - final double dLong = Math.toRadians(endLong - startLong); - - final double a = Math.pow(Math.sin(dLat / 2), 2) - + Math.cos(Math.toRadians(startLat)) * Math.cos(Math.toRadians(endLat)) - * Math.pow(Math.sin(dLong / 2), 2); - final double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return EARTH_RADIUS_MI * c; - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/GeoLocation.java b/src/main/java/org/prebid/server/deals/targeting/model/GeoLocation.java deleted file mode 100644 index 4918d500e13..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/model/GeoLocation.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.targeting.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class GeoLocation { - - Float lat; - - Float lon; -} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/GeoRegion.java b/src/main/java/org/prebid/server/deals/targeting/model/GeoRegion.java deleted file mode 100644 index f7ae09c5ffd..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/model/GeoRegion.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.deals.targeting.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class GeoRegion { - - Float lat; - - Float lon; - - @JsonProperty("radiusMiles") - Float radiusMiles; -} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/LookupResult.java b/src/main/java/org/prebid/server/deals/targeting/model/LookupResult.java deleted file mode 100644 index 557fa98ec4a..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/model/LookupResult.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.prebid.server.deals.targeting.model; - -import lombok.Value; -import org.apache.commons.collections4.ListUtils; - -import java.util.Collections; -import java.util.List; -import java.util.function.Predicate; - -@Value(staticConstructor = "of") -public class LookupResult { - - private static final LookupResult EMPTY = LookupResult.of(Collections.emptyList()); - - List values; - - @SuppressWarnings("unchecked") - public static LookupResult empty() { - return (LookupResult) EMPTY; - } - - public static LookupResult ofValue(T value) { - return LookupResult.of(Collections.singletonList(value)); - } - - public boolean anyMatch(Predicate matcher) { - return values.stream().anyMatch(matcher); - } - - public LookupResult orElse(List orValues) { - return LookupResult.of(ListUtils.union(values, orValues)); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/Size.java b/src/main/java/org/prebid/server/deals/targeting/model/Size.java deleted file mode 100644 index 5b54b220596..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/model/Size.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.deals.targeting.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class Size { - - Integer w; - - Integer h; -} diff --git a/src/main/java/org/prebid/server/deals/targeting/syntax/BooleanOperator.java b/src/main/java/org/prebid/server/deals/targeting/syntax/BooleanOperator.java deleted file mode 100644 index 534ed8f4fc8..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/syntax/BooleanOperator.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.prebid.server.deals.targeting.syntax; - -import java.util.Arrays; - -public enum BooleanOperator { - - AND("$and"), - OR("$or"), - NOT("$not"); - - private final String value; - - BooleanOperator(String value) { - this.value = value; - } - - public static boolean isBooleanOperator(String candidate) { - return Arrays.stream(BooleanOperator.values()).anyMatch(op -> op.value.equals(candidate)); - } - - public static BooleanOperator fromString(String candidate) { - for (final BooleanOperator op : values()) { - if (op.value.equals(candidate)) { - return op; - } - } - throw new IllegalArgumentException("Unrecognized boolean operator: " + candidate); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/syntax/MatchingFunction.java b/src/main/java/org/prebid/server/deals/targeting/syntax/MatchingFunction.java deleted file mode 100644 index 54bb4a78fb7..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/syntax/MatchingFunction.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.prebid.server.deals.targeting.syntax; - -import java.util.Arrays; - -public enum MatchingFunction { - - MATCHES("$matches"), - IN("$in"), - INTERSECTS("$intersects"), - WITHIN("$within"); - - private final String value; - - MatchingFunction(String value) { - this.value = value; - } - - public String value() { - return value; - } - - public static boolean isMatchingFunction(String candidate) { - return Arrays.stream(MatchingFunction.values()).anyMatch(op -> op.value.equals(candidate)); - } - - public static MatchingFunction fromString(String candidate) { - for (final MatchingFunction op : values()) { - if (op.value.equals(candidate)) { - return op; - } - } - throw new IllegalArgumentException("Unrecognized matching function: " + candidate); - } -} diff --git a/src/main/java/org/prebid/server/deals/targeting/syntax/TargetingCategory.java b/src/main/java/org/prebid/server/deals/targeting/syntax/TargetingCategory.java deleted file mode 100644 index b7461807420..00000000000 --- a/src/main/java/org/prebid/server/deals/targeting/syntax/TargetingCategory.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.prebid.server.deals.targeting.syntax; - -import lombok.EqualsAndHashCode; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.exception.TargetingSyntaxException; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.Objects; - -@EqualsAndHashCode -public class TargetingCategory { - - private static final String BIDDER_PARAM_PATH_PATTERN = "\\w+(\\.\\w+)+"; - - private static final EnumSet DYNAMIC_TYPES = EnumSet.of( - Type.deviceGeoExt, - Type.deviceExt, - Type.bidderParam, - Type.userSegment, - Type.userFirstPartyData, - Type.siteFirstPartyData); - - private static final EnumSet STATIC_TYPES = EnumSet.complementOf(DYNAMIC_TYPES); - - private final Type type; - private final String path; - - public TargetingCategory(Type type) { - this(type, null); - } - - public TargetingCategory(Type type, String path) { - this.type = Objects.requireNonNull(type); - this.path = path; - } - - public static boolean isTargetingCategory(String candidate) { - final boolean isSimpleCategory = STATIC_TYPES.stream().anyMatch(op -> op.attribute().equals(candidate)); - return isSimpleCategory || DYNAMIC_TYPES.stream().anyMatch(op -> candidate.startsWith(op.attribute())); - } - - public static TargetingCategory fromString(String candidate) { - for (final Type type : STATIC_TYPES) { - if (type.attribute().equals(candidate)) { - return new TargetingCategory(type); - } - } - - for (final Type type : DYNAMIC_TYPES) { - if (candidate.startsWith(type.attribute())) { - return parseDynamicCategory(candidate, type); - } - } - - throw new IllegalArgumentException("Unrecognized targeting category: " + candidate); - } - - private static TargetingCategory parseDynamicCategory(String candidate, Type type) { - return switch (type) { - case deviceGeoExt, deviceExt, userSegment, userFirstPartyData, siteFirstPartyData -> - parseByTypeAttribute(candidate, type); - case bidderParam -> parseBidderParam(candidate, type); - default -> throw new IllegalStateException("Unexpected dynamic targeting category type " + type); - }; - } - - private static TargetingCategory parseByTypeAttribute(String candidate, Type type) { - final String candidatePath = StringUtils.substringAfter(candidate, type.attribute()); - return new TargetingCategory(type, candidatePath); - } - - private static TargetingCategory parseBidderParam(String candidate, Type type) { - final String candidatePath = StringUtils.substringAfter(candidate, type.attribute()); - if (candidatePath.matches(BIDDER_PARAM_PATH_PATTERN)) { - return new TargetingCategory(type, dropBidderName(candidatePath)); - } else { - throw new TargetingSyntaxException("BidderParam path is incorrect: " + candidatePath); - } - } - - private static String dropBidderName(String path) { - final int index = path.indexOf('.'); - return path.substring(index + 1); - } - - public Type type() { - return type; - } - - public String path() { - return path; - } - - public enum Type { - size("adunit.size"), - mediaType("adunit.mediatype"), - adslot("adunit.adslot"), - domain("site.domain"), - publisherDomain("site.publisher.domain"), - referrer("site.referrer"), - appBundle("app.bundle"), - deviceGeoExt("device.geo.ext."), - deviceExt("device.ext."), - pagePosition("pos"), - location("geo.distance"), - bidderParam("bidp."), - userSegment("segment."), - userFirstPartyData("ufpd."), - siteFirstPartyData("sfpd."), - dow("user.ext.time.userdow"), - hour("user.ext.time.userhour"); - - private final String attribute; - - Type(String attribute) { - this.attribute = attribute; - } - - public String attribute() { - return attribute; - } - - public static Type fromString(String attribute) { - return Arrays.stream(values()) - .filter(value -> value.attribute.equals(attribute)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException( - "Unrecognized targeting category type: " + attribute)); - } - } -} diff --git a/src/main/java/org/prebid/server/events/EventRequest.java b/src/main/java/org/prebid/server/events/EventRequest.java index 59ee222c82b..64e430e3945 100644 --- a/src/main/java/org/prebid/server/events/EventRequest.java +++ b/src/main/java/org/prebid/server/events/EventRequest.java @@ -28,8 +28,6 @@ public class EventRequest { Analytics analytics; - String lineItemId; - public enum Type { win, imp diff --git a/src/main/java/org/prebid/server/events/EventUtil.java b/src/main/java/org/prebid/server/events/EventUtil.java index efb9f4137f9..864892e6725 100644 --- a/src/main/java/org/prebid/server/events/EventUtil.java +++ b/src/main/java/org/prebid/server/events/EventUtil.java @@ -37,8 +37,6 @@ public class EventUtil { private static final String ENABLED_ANALYTICS = "1"; // default private static final String DISABLED_ANALYTICS = "0"; - private static final String LINE_ITEM_ID_PARAMETER = "l"; - private EventUtil() { } @@ -142,7 +140,6 @@ public static EventRequest from(RoutingContext routingContext) { .format(format) .analytics(analytics) .integration(queryParams.get(INTEGRATION_PARAMETER)) - .lineItemId(queryParams.get(LINE_ITEM_ID_PARAMETER)) .build(); } @@ -192,10 +189,6 @@ private static String optionalParameters(EventRequest eventRequest) { result.append(nameValueAsQueryString(ANALYTICS_PARAMETER, DISABLED_ANALYTICS)); } - result.append(StringUtils.isNotEmpty(eventRequest.getLineItemId()) - ? nameValueAsQueryString(LINE_ITEM_ID_PARAMETER, eventRequest.getLineItemId()) - : StringUtils.EMPTY); // skip parameter - return result.toString(); } diff --git a/src/main/java/org/prebid/server/events/EventsService.java b/src/main/java/org/prebid/server/events/EventsService.java index 59accc7037e..819dfeb0e0a 100644 --- a/src/main/java/org/prebid/server/events/EventsService.java +++ b/src/main/java/org/prebid/server/events/EventsService.java @@ -19,16 +19,15 @@ public EventsService(String externalUrl) { public Events createEvent(String bidId, String bidder, String accountId, - String lineItemId, boolean analyticsEnabled, EventsContext eventsContext) { + return Events.of( eventUrl( EventRequest.Type.win, bidId, bidder, accountId, - lineItemId, analytics(analyticsEnabled), EventRequest.Format.image, eventsContext), @@ -37,7 +36,6 @@ public Events createEvent(String bidId, bidId, bidder, accountId, - lineItemId, analytics(analyticsEnabled), EventRequest.Format.image, eventsContext)); @@ -46,14 +44,17 @@ public Events createEvent(String bidId, /** * Returns url for win tracking. */ - public String winUrl(String bidId, String bidder, String accountId, String lineItemId, - boolean analyticsEnabled, EventsContext eventsContext) { + public String winUrl(String bidId, + String bidder, + String accountId, + boolean analyticsEnabled, + EventsContext eventsContext) { + return eventUrl( EventRequest.Type.win, bidId, bidder, accountId, - lineItemId, analytics(analyticsEnabled), EventRequest.Format.image, eventsContext); @@ -65,13 +66,12 @@ public String winUrl(String bidId, String bidder, String accountId, String lineI public String vastUrlTracking(String bidId, String bidder, String accountId, - String lineItemId, EventsContext eventsContext) { + return eventUrl(EventRequest.Type.imp, bidId, bidder, accountId, - lineItemId, null, EventRequest.Format.blank, eventsContext); @@ -81,7 +81,6 @@ private String eventUrl(EventRequest.Type type, String bidId, String bidder, String accountId, - String lineItemId, EventRequest.Analytics analytics, EventRequest.Format format, EventsContext eventsContext) { @@ -95,7 +94,6 @@ private String eventUrl(EventRequest.Type type, .timestamp(eventsContext.getAuctionTimestamp()) .format(format) .integration(eventsContext.getIntegration()) - .lineItemId(lineItemId) .analytics(analytics) .build(); diff --git a/src/main/java/org/prebid/server/exception/BlacklistedAccountException.java b/src/main/java/org/prebid/server/exception/BlacklistedAccountException.java deleted file mode 100644 index afe1fdc4d2a..00000000000 --- a/src/main/java/org/prebid/server/exception/BlacklistedAccountException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.prebid.server.exception; - -public class BlacklistedAccountException extends RuntimeException { - - public BlacklistedAccountException(String message) { - super(message); - } -} diff --git a/src/main/java/org/prebid/server/exception/BlacklistedAppException.java b/src/main/java/org/prebid/server/exception/BlacklistedAppException.java deleted file mode 100644 index 53707383798..00000000000 --- a/src/main/java/org/prebid/server/exception/BlacklistedAppException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.prebid.server.exception; - -public class BlacklistedAppException extends RuntimeException { - - public BlacklistedAppException(String message) { - super(message); - } -} diff --git a/src/main/java/org/prebid/server/exception/BlocklistedAccountException.java b/src/main/java/org/prebid/server/exception/BlocklistedAccountException.java new file mode 100644 index 00000000000..6f88b7b0a04 --- /dev/null +++ b/src/main/java/org/prebid/server/exception/BlocklistedAccountException.java @@ -0,0 +1,8 @@ +package org.prebid.server.exception; + +public class BlocklistedAccountException extends RuntimeException { + + public BlocklistedAccountException(String message) { + super(message); + } +} diff --git a/src/main/java/org/prebid/server/exception/BlocklistedAppException.java b/src/main/java/org/prebid/server/exception/BlocklistedAppException.java new file mode 100644 index 00000000000..7774a02334b --- /dev/null +++ b/src/main/java/org/prebid/server/exception/BlocklistedAppException.java @@ -0,0 +1,8 @@ +package org.prebid.server.exception; + +public class BlocklistedAppException extends RuntimeException { + + public BlocklistedAppException(String message) { + super(message); + } +} diff --git a/src/main/java/org/prebid/server/exception/TargetingSyntaxException.java b/src/main/java/org/prebid/server/exception/TargetingSyntaxException.java deleted file mode 100644 index b5fbd75b47d..00000000000 --- a/src/main/java/org/prebid/server/exception/TargetingSyntaxException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.exception; - -public class TargetingSyntaxException extends RuntimeException { - - public TargetingSyntaxException(String message) { - super(message); - } - - public TargetingSyntaxException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java index f47faf082a7..b841bf8a136 100644 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java @@ -1,48 +1,51 @@ package org.prebid.server.execution; -import io.vertx.core.AsyncResult; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; -import io.vertx.core.file.AsyncFile; import io.vertx.core.file.CopyOptions; import io.vertx.core.file.FileProps; import io.vertx.core.file.FileSystem; import io.vertx.core.file.FileSystemException; import io.vertx.core.file.OpenOptions; import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.http.HttpHeaders; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.core.streams.Pump; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.retry.Retryable; import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.retry.Retryable; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Paths; import java.util.Objects; -import java.util.concurrent.TimeoutException; public class RemoteFileSyncer { private static final Logger logger = LoggerFactory.getLogger(RemoteFileSyncer.class); + private final RemoteFileProcessor processor; private final String downloadUrl; private final String saveFilePath; private final String tmpFilePath; private final RetryPolicy retryPolicy; - private final long timeout; private final long updatePeriod; private final HttpClient httpClient; private final Vertx vertx; private final FileSystem fileSystem; + private final RequestOptions getFileRequestOptions; + private final RequestOptions isUpdateRequiredRequestOptions; - public RemoteFileSyncer(String downloadUrl, + public RemoteFileSyncer(RemoteFileProcessor processor, + String downloadUrl, String saveFilePath, String tmpFilePath, RetryPolicy retryPolicy, @@ -51,11 +54,11 @@ public RemoteFileSyncer(String downloadUrl, HttpClient httpClient, Vertx vertx) { + this.processor = Objects.requireNonNull(processor); this.downloadUrl = HttpUtil.validateUrl(downloadUrl); this.saveFilePath = Objects.requireNonNull(saveFilePath); this.tmpFilePath = Objects.requireNonNull(tmpFilePath); this.retryPolicy = Objects.requireNonNull(retryPolicy); - this.timeout = timeout; this.updatePeriod = updatePeriod; this.httpClient = Objects.requireNonNull(httpClient); this.vertx = Objects.requireNonNull(vertx); @@ -63,6 +66,18 @@ public RemoteFileSyncer(String downloadUrl, createAndCheckWritePermissionsFor(fileSystem, saveFilePath); createAndCheckWritePermissionsFor(fileSystem, tmpFilePath); + + getFileRequestOptions = new RequestOptions() + .setMethod(HttpMethod.GET) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); + + isUpdateRequiredRequestOptions = new RequestOptions() + .setMethod(HttpMethod.HEAD) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); } private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { @@ -79,197 +94,96 @@ private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, Str } } - public void sync(RemoteFileProcessor processor) { - isFileExists(saveFilePath) - .compose(exists -> exists ? processSavedFile(processor) : syncRemoteFiles(retryPolicy)) - .onComplete(syncResult -> handleSync(processor, syncResult)); - } - - private Future isFileExists(String filePath) { - final Promise promise = Promise.promise(); - fileSystem.exists(filePath, async -> { - if (async.succeeded()) { - promise.complete(async.result()); - } else { - promise.fail("Cant check if file exists " + filePath); - } - }); - return promise.future(); + public void sync() { + fileSystem.exists(saveFilePath) + .compose(exists -> exists ? processSavedFile() : syncRemoteFile(retryPolicy)) + .onComplete(ignored -> setUpDeferredUpdate()); } - private Future processSavedFile(RemoteFileProcessor processor) { + private Future processSavedFile() { return processor.setDataPath(saveFilePath) - .map(false) - .recover(ignored -> removeCorruptedSaveFile()); + .onFailure(error -> logger.error("Can't process saved file: " + saveFilePath)) + .recover(ignored -> deleteFile(saveFilePath).mapEmpty()) + .mapEmpty(); } - private Future removeCorruptedSaveFile() { - return deleteFileIfExists(saveFilePath) - .compose(ignored -> syncRemoteFiles(retryPolicy)) - .recover(error -> Future.failedFuture(new PreBidException( - "Corrupted file %s can't be deleted. Please check permission or delete manually." - .formatted(saveFilePath), error))); + private Future deleteFile(String filePath) { + return fileSystem.delete(filePath) + .onFailure(error -> logger.error("Can't delete corrupted file: " + saveFilePath)); } - private Future syncRemoteFiles(RetryPolicy retryPolicy) { - return deleteFileIfExists(tmpFilePath) - .compose(ignored -> downloadToTempFile()) - .recover(error -> retrySync(retryPolicy)) - .compose(downloadResult -> swapFiles()) - .map(true); - } + private Future syncRemoteFile(RetryPolicy retryPolicy) { + return fileSystem.open(tmpFilePath, new OpenOptions()) - private Future deleteFileIfExists(String filePath) { - return isFileExists(filePath) - .compose(exists -> exists ? deleteFile(filePath) : Future.succeededFuture()); - } + .compose(tmpFile -> sendHttpRequest(getFileRequestOptions) + .compose(response -> response.pipeTo(tmpFile)) + .onComplete(result -> tmpFile.close())) - private Future deleteFile(String filePath) { - final Promise promise = Promise.promise(); - fileSystem.delete(filePath, promise); - return promise.future(); - } + .compose(ignored -> fileSystem.move( + tmpFilePath, saveFilePath, new CopyOptions().setReplaceExisting(true))) - private Future downloadToTempFile() { - return openFile(tmpFilePath) - .compose(tmpFile -> requestData() - .compose(response -> pumpToFile(response, tmpFile))); - } + .compose(ignored -> processSavedFile()) + .onFailure(ignored -> deleteFile(tmpFilePath)) + .onFailure(error -> logger.error("Could not sync remote file", error)) + + .recover(error -> retrySync(retryPolicy).mapEmpty()) + .mapEmpty(); - private Future requestData() { - final Promise promise = Promise.promise(); - httpClient.getAbs(downloadUrl, promise::complete).end(); - return promise.future(); } private Future retrySync(RetryPolicy retryPolicy) { if (retryPolicy instanceof Retryable policy) { - logger.info("Retrying file download from {0} with policy: {1}", downloadUrl, retryPolicy); + logger.info("Retrying file download from {} with policy: {}", downloadUrl, retryPolicy); final Promise promise = Promise.promise(); - vertx.setTimer(policy.delay(), timerId -> - syncRemoteFiles(policy.next()) - .onFailure(promise::fail) - .onSuccess(ignored -> promise.complete())); - + vertx.setTimer(policy.delay(), timerId -> syncRemoteFile(policy.next()).onComplete(promise)); return promise.future(); } else { return Future.failedFuture(new PreBidException("File sync failed")); } } - private Future openFile(String path) { - final Promise promise = Promise.promise(); - fileSystem.open(path, new OpenOptions().setCreateNew(true), promise); - return promise.future(); - } - - private Future pumpToFile(HttpClientResponse httpClientResponse, AsyncFile asyncFile) { - final Promise promise = Promise.promise(); - logger.info("Trying to download file from {0}", downloadUrl); - httpClientResponse.pause(); - - final Pump pump = Pump.pump(httpClientResponse, asyncFile); - pump.start(); - - httpClientResponse.resume(); - final long timeoutTimerId = setTimeoutTimer(asyncFile, pump, promise); - httpClientResponse.endHandler(responseEndResult -> handleResponseEnd(asyncFile, timeoutTimerId, promise)); - - return promise.future(); - } - - private long setTimeoutTimer(AsyncFile asyncFile, Pump pump, Promise promise) { - return vertx.setTimer(timeout, timerId -> handleTimeout(asyncFile, pump, promise)); - } - - private void handleTimeout(AsyncFile asyncFile, Pump pump, Promise promise) { - pump.stop(); - asyncFile.close(); - if (!promise.future().isComplete()) { - promise.fail(new TimeoutException("Timeout on download")); - } - } - - private void handleResponseEnd(AsyncFile asyncFile, long idTimer, Promise promise) { - vertx.cancelTimer(idTimer); - asyncFile.flush().close(promise); - } - - private Future swapFiles() { - final Promise promise = Promise.promise(); - logger.info("Sync {0} to {1}", tmpFilePath, saveFilePath); - - final CopyOptions copyOptions = new CopyOptions().setReplaceExisting(true); - fileSystem.move(tmpFilePath, saveFilePath, copyOptions, promise); - return promise.future(); - } - - private void handleSync(RemoteFileProcessor remoteFileProcessor, AsyncResult syncResult) { - if (syncResult.succeeded()) { - if (syncResult.result()) { - logger.info("Sync service for {0}", saveFilePath); - remoteFileProcessor.setDataPath(saveFilePath) - .onComplete(this::logFileProcessStatus); - } else { - logger.info("Sync is not required for {0}", saveFilePath); - } - } else { - logger.error("Cant sync file from {0}", syncResult.cause(), downloadUrl); - } - - // setup new update regardless of the result + private void setUpDeferredUpdate() { if (updatePeriod > 0) { - vertx.setTimer(updatePeriod, idUpdateNew -> configureAutoUpdates(remoteFileProcessor)); + vertx.setPeriodic(updatePeriod, ignored -> updateIfNeeded()); } } - private void logFileProcessStatus(AsyncResult serviceRespond) { - if (serviceRespond.succeeded()) { - logger.info("Service successfully received file {0}.", saveFilePath); + private void updateIfNeeded() { + sendHttpRequest(isUpdateRequiredRequestOptions) + .compose(response -> fileSystem.exists(saveFilePath) + .compose(exists -> exists + ? isLengthChanged(response) + : Future.succeededFuture(true))) + .onSuccess(shouldUpdate -> { + if (shouldUpdate) { + syncRemoteFile(retryPolicy); + } + }); + } + + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .compose(this::validateResponse); + } + + private Future validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + return Future.failedFuture(new PreBidException( + String.format("Got unexpected response from server with status code %s and message %s", + statusCode, + response.statusMessage()))); } else { - logger.error("Service cant process file {0} and still unavailable.", saveFilePath); + return Future.succeededFuture(response); } } - private void configureAutoUpdates(RemoteFileProcessor remoteFileProcessor) { - logger.info("Check for updated for {0}", saveFilePath); - tryUpdate().onComplete(asyncUpdate -> { - if (asyncUpdate.failed()) { - logger.warn("File {0} update failed", asyncUpdate.cause(), saveFilePath); - } - handleSync(remoteFileProcessor, asyncUpdate); - }); - } - - private Future tryUpdate() { - return isFileExists(saveFilePath) - .compose(fileExists -> fileExists ? isUpdateRequired() : Future.succeededFuture(true)) - .compose(needUpdate -> needUpdate ? syncRemoteFiles(retryPolicy) : Future.succeededFuture(false)); - } - - private Future isUpdateRequired() { - final Promise isUpdateRequired = Promise.promise(); - httpClient.headAbs(downloadUrl, response -> checkNewVersion(response, isUpdateRequired)) - .exceptionHandler(isUpdateRequired::fail) - .end(); - return isUpdateRequired.future(); - } - - private void checkNewVersion(HttpClientResponse response, Promise isUpdateRequired) { + private Future isLengthChanged(HttpClientResponse response) { final String contentLengthParameter = response.getHeader(HttpHeaders.CONTENT_LENGTH); - if (StringUtils.isNumeric(contentLengthParameter) && !contentLengthParameter.equals("0")) { - final long contentLength = Long.parseLong(contentLengthParameter); - fileSystem.props(saveFilePath, filePropsResult -> { - if (filePropsResult.succeeded()) { - logger.info("Prev length = {0}, new length = {1}", filePropsResult.result().size(), contentLength); - isUpdateRequired.complete(filePropsResult.result().size() != contentLength); - } else { - isUpdateRequired.fail(filePropsResult.cause()); - } - }); - } else { - isUpdateRequired.fail("ContentLength is invalid: " + contentLengthParameter); - } + return StringUtils.isNumeric(contentLengthParameter) && !contentLengthParameter.equals("0") + ? fileSystem.props(saveFilePath).map(props -> props.size() != Long.parseLong(contentLengthParameter)) + : Future.failedFuture("ContentLength is invalid: " + contentLengthParameter); } } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java index 0dd52cb8ff7..6238b17eec2 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java @@ -4,6 +4,7 @@ import com.iab.openrtb.request.Imp; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidder.model.Price; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -19,12 +20,17 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; public class BasicPriceFloorAdjuster implements PriceFloorAdjuster { private static final int ADJUSTMENT_SCALE = 4; + private static final BiFunction DIVIDE_FUNCTION = + (priceFloor, factor) -> priceFloor.divide(factor, ADJUSTMENT_SCALE, RoundingMode.HALF_EVEN); + private static final BiFunction MULTIPLY_FUNCTION = BigDecimal::multiply; private final FloorAdjustmentFactorResolver floorAdjustmentFactorResolver; @@ -33,21 +39,42 @@ public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFact } @Override - public BigDecimal adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { + public Price adjustForImp(Imp imp, + String bidder, + BidRequest bidRequest, + Account account, + List debugWarnings) { + + return adjust(imp, bidder, bidRequest, account, DIVIDE_FUNCTION); + } + + @Override + public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { + return adjust(imp, bidder, bidRequest, account, MULTIPLY_FUNCTION); + } + + private Price adjust(Imp imp, + String bidder, + BidRequest bidRequest, + Account account, + BiFunction function) { + final ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); final BigDecimal impBidFloor = imp.getBidfloor(); if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null || extractBidAdjustmentFactors == null) { - return impBidFloor; + return Price.of(imp.getBidfloorcur(), impBidFloor); } final Set impMediaTypes = retrieveImpMediaTypes(imp); final BigDecimal factor = floorAdjustmentFactorResolver.resolve( impMediaTypes, extractBidAdjustmentFactors, bidder); - return factor != null - ? BidderUtil.roundFloor(impBidFloor.divide(factor, ADJUSTMENT_SCALE, RoundingMode.HALF_EVEN)) + final BigDecimal adjustedBidFloor = factor != null && factor.compareTo(BigDecimal.ONE) != 0 + ? BidderUtil.roundFloor(function.apply(impBidFloor, factor)) : impBidFloor; + + return Price.of(imp.getBidfloorcur(), adjustedBidFloor); } private static boolean shouldAdjustBidFloor(BidRequest bidRequest, Account account) { diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 9e8734576bb..ee77a6bbcb5 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -3,8 +3,6 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; @@ -18,12 +16,15 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.PriceFloorInfo; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -49,10 +50,15 @@ public class BasicPriceFloorEnforcer implements PriceFloorEnforcer { private static final int ENFORCE_RATE_MAX = 100; private final CurrencyConversionService currencyConversionService; + private final PriceFloorAdjuster priceFloorAdjuster; private final Metrics metrics; - public BasicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, Metrics metrics) { + public BasicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, + PriceFloorAdjuster priceFloorAdjuster, + Metrics metrics) { + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); this.metrics = Objects.requireNonNull(metrics); } @@ -135,7 +141,12 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); final BidderSeatBid seatBid = ObjectUtil.getIfNotNull(bidderResponse, BidderResponse::getSeatBid); final List bidderBids = ObjectUtil.getIfNotNull(seatBid, BidderSeatBid::getBids); - if (CollectionUtils.isEmpty(bidderBids)) { + + final BidRequest bidderBidRequest = Optional.ofNullable(auctionParticipation.getBidderRequest()) + .map(BidderRequest::getBidRequest) + .orElse(null); + + if (CollectionUtils.isEmpty(bidderBids) || bidderBidRequest == null) { return auctionParticipation; } @@ -143,9 +154,6 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, final List errors = new ArrayList<>(seatBid.getErrors()); final List warnings = new ArrayList<>(seatBid.getWarnings()); - final BidRequest bidderBidRequest = Optional.ofNullable(auctionParticipation.getBidderRequest()) - .map(BidderRequest::getBidRequest) - .orElse(null); final boolean enforceDealFloors = enforceDealFloors(auctionParticipation, account); for (BidderBid bidderBid : bidderBids) { @@ -157,7 +165,13 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, } final BigDecimal price = bid.getPrice(); - final BigDecimal floor = resolveFloor(bidderBid, bidderBidRequest, bidRequest, errors); + final BigDecimal floor = resolveFloor( + bidderResponse.getBidder(), + account, + bidderBid, + bidderBidRequest, + bidRequest, + errors); if (isPriceBelowFloor(price, floor)) { final String impId = bid.getImpid(); @@ -165,7 +179,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, "Bid with id '%s' was rejected by floor enforcement: price %s is below the floor %s" .formatted(bid.getId(), price, floor), impId)); - rejectionTracker.reject(impId, BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); + rejectionTracker.reject(impId, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); updatedBidderBids.remove(bidderBid); } } @@ -197,7 +211,9 @@ private static boolean enforceDealFloors(AuctionParticipation auctionParticipati return BooleanUtils.isTrue(requestEnforceDealFloors) && BooleanUtils.isTrue(accountEnforceDealFloors); } - private BigDecimal resolveFloor(BidderBid bidderBid, + private BigDecimal resolveFloor(String bidder, + Account account, + BidderBid bidderBid, BidRequest bidderBidRequest, BidRequest bidRequest, List errors) { @@ -210,9 +226,15 @@ private BigDecimal resolveFloor(BidderBid bidderBid, return convertIfRequired(customBidderFloor, priceFloorInfo.getCurrency(), bidderBidRequest, bidRequest); } - final Imp imp = correspondingImp(bidderBid.getBid(), bidRequest.getImp()); + final Imp imp = correspondingImp(bidderBid.getBid(), bidderBidRequest.getImp()); + final Price correctedImpFloor = priceFloorAdjuster.revertAdjustmentForImp(imp, bidder, bidRequest, account); final String bidRequestCurrency = resolveBidRequestCurrency(bidRequest); - return convertCurrency(imp.getBidfloor(), bidRequest, imp.getBidfloorcur(), bidRequestCurrency); + + return convertCurrency( + correctedImpFloor.getValue(), + bidRequest, + correctedImpFloor.getCurrency(), + bidRequestCurrency); } catch (PreBidException e) { final String logMessage = "Price floors enforcement failed for request id: %s, reason: %s" .formatted(bidRequest.getId(), e.getMessage()); @@ -261,7 +283,7 @@ private BigDecimal convertCurrency(BigDecimal floor, private static String resolveBidRequestCurrency(BidRequest bidRequest) { final List currencies = ObjectUtil.getIfNotNull(bidRequest, BidRequest::getCur); - return CollectionUtils.isEmpty(currencies) ? null : currencies.get(0); + return CollectionUtils.isEmpty(currencies) ? null : currencies.getFirst(); } private static Imp correspondingImp(Bid bid, List imps) { diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index f5cf62aeb72..c8163131c6e 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -4,13 +4,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.model.Price; import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorData; @@ -22,6 +19,8 @@ import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; @@ -71,20 +70,18 @@ private static int resolveModelGroupWeight(PriceFloorModelGroup modelGroup) { } @Override - public AuctionContext enrichWithPriceFloors(AuctionContext auctionContext) { - final Account account = auctionContext.getAccount(); - final BidRequest bidRequest = auctionContext.getBidRequest(); - final List errors = auctionContext.getPrebidErrors(); - final List warnings = auctionContext.getDebugWarnings(); + public BidRequest enrichWithPriceFloors(BidRequest bidRequest, + Account account, + String bidder, + List errors, + List warnings) { if (isPriceFloorsDisabled(account, bidRequest)) { - return auctionContext.with(disableFloorsForRequest(bidRequest)); + return disableFloorsForRequest(bidRequest); } final PriceFloorRules floors = resolveFloors(account, bidRequest, errors); - final BidRequest updatedBidRequest = updateBidRequestWithFloors(bidRequest, floors, errors, warnings); - - return auctionContext.with(updatedBidRequest); + return updateBidRequestWithFloors(bidRequest, bidder, floors, errors, warnings); } private static boolean isPriceFloorsDisabled(Account account, BidRequest bidRequest) { @@ -242,6 +239,7 @@ private static String resolveFloorProvider(PriceFloorRules rules) { } private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, + String bidder, PriceFloorRules floors, List errors, List warnings) { @@ -251,7 +249,7 @@ private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, final List imps = skipFloors ? bidRequest.getImp() - : updateImpsWithFloors(floors, bidRequest, errors, warnings); + : updateImpsWithFloors(floors, bidRequest, bidder, errors, warnings); final ExtRequest extRequest = updateExtRequestWithFloors(bidRequest, floors, requestSkipRate, skipFloors); return bidRequest.toBuilder() @@ -291,22 +289,24 @@ private static boolean isValidSkipRate(Integer value) { private List updateImpsWithFloors(PriceFloorRules effectiveFloors, BidRequest bidRequest, + String bidder, List errors, List warnings) { final List imps = bidRequest.getImp(); final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); - final PriceFloorRules floors = - ObjectUtils.defaultIfNull(effectiveFloors, - ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors)); + final PriceFloorRules floors = ObjectUtils.defaultIfNull( + effectiveFloors, + ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors)); + final PriceFloorModelGroup modelGroup = extractFloorModelGroup(floors); if (modelGroup == null) { return imps; } return CollectionUtils.emptyIfNull(imps).stream() - .map(imp -> updateImpWithFloors(imp, floors, bidRequest, errors, warnings)) + .map(imp -> updateImpWithFloors(imp, bidder, floors, bidRequest, errors, warnings)) .toList(); } @@ -314,10 +314,11 @@ private static PriceFloorModelGroup extractFloorModelGroup(PriceFloorRules floor final PriceFloorData data = ObjectUtil.getIfNotNull(floors, PriceFloorRules::getData); final List modelGroups = ObjectUtil.getIfNotNull(data, PriceFloorData::getModelGroups); - return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.get(0) : null; + return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.getFirst() : null; } private Imp updateImpWithFloors(Imp imp, + String bidder, PriceFloorRules floorRules, BidRequest bidRequest, List errors, @@ -325,7 +326,7 @@ private Imp updateImpWithFloors(Imp imp, final PriceFloorResult priceFloorResult; try { - priceFloorResult = floorResolver.resolve(bidRequest, floorRules, imp, warnings); + priceFloorResult = floorResolver.resolve(bidRequest, floorRules, imp, bidder, warnings); } catch (IllegalStateException e) { errors.add("Cannot resolve bid floor, error: " + e.getMessage()); return imp; diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java index 6ce588f9c68..f726755e9f8 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java @@ -15,8 +15,6 @@ import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Video; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; @@ -35,6 +33,8 @@ import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -59,12 +59,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class BasicPriceFloorResolver implements PriceFloorResolver { @@ -115,6 +115,7 @@ public PriceFloorResult resolve(BidRequest bidRequest, Imp imp, ImpMediaType mediaType, Format format, + String bidder, List warnings) { if (isPriceFloorsDisabledForRequest(bidRequest)) { @@ -141,7 +142,7 @@ public PriceFloorResult resolve(BidRequest bidRequest, WILDCARD_CATCH_ALL, ObjectUtils.defaultIfNull(schema.getDelimiter(), SCHEMA_DEFAULT_DELIMITER), values.keySet()); - final PrebidConfigParameters parameters = createParameters(schema, bidRequest, imp, mediaType, format); + final PrebidConfigParameters parameters = createParameters(schema, bidRequest, imp, mediaType, format, bidder); final String rule = matchingStrategy.match(source, parameters); final BigDecimal floorForRule = rule != null ? values.get(rule) : null; @@ -184,26 +185,30 @@ private static PriceFloorModelGroup extractFloorModelGroup(PriceFloorRules floor final PriceFloorData data = ObjectUtil.getIfNotNull(floors, PriceFloorRules::getData); final List modelGroups = ObjectUtil.getIfNotNull(data, PriceFloorData::getModelGroups); - return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.get(0) : null; + return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.getFirst() : null; } private static Map keysToLowerCase(Map map) { return map.entrySet().stream() - .collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue)); + .collect( + HashMap::new, + (hashMap, entry) -> hashMap.put(entry.getKey().toLowerCase(), entry.getValue()), + HashMap::putAll); } private PrebidConfigParameters createParameters(PriceFloorSchema schema, BidRequest bidRequest, Imp imp, ImpMediaType mediaType, - Format format) { + Format format, + String bidder) { final List resolvedMediaTypes = mediaType != null ? Collections.singletonList(mediaType) : mediaTypesFromImp(imp); final List conditionsMatchers = schema.getFields().stream() - .map(field -> createParameter(field, bidRequest, imp, resolvedMediaTypes, format)) + .map(field -> createParameter(field, bidRequest, imp, resolvedMediaTypes, format, bidder)) .toList(); return SimpleParameters.of(conditionsMatchers); @@ -240,7 +245,8 @@ private PrebidConfigParameter createParameter(PriceFloorField field, BidRequest bidRequest, Imp imp, List mediaTypes, - Format format) { + Format format, + String bidder) { return switch (field) { case siteDomain -> siteDomainFromRequest(bidRequest); @@ -254,6 +260,7 @@ private PrebidConfigParameter createParameter(PriceFloorField field, case adUnitCode -> adUnitCodeFromImp(imp); case country -> countryFromRequest(bidRequest); case deviceType -> resolveDeviceTypeFromRequest(bidRequest); + case bidder -> SimpleDirectParameter.of(bidder); }; } @@ -313,7 +320,7 @@ private static PrebidConfigParameter mediaTypeFrom(List impMediaTy return PrebidConfigParameter.wildcard(); } - final ImpMediaType impMediaType = impMediaTypes.get(0); + final ImpMediaType impMediaType = impMediaTypes.getFirst(); return impMediaType == ImpMediaType.video ? SimpleDirectParameter.of(List.of(impMediaType.toString(), VIDEO_ALIAS)) : SimpleDirectParameter.of(impMediaType.toString()); @@ -331,7 +338,7 @@ private static Format resolveFormatFromImp(Imp imp, List mediaType return null; } - return switch (mediaTypes.get(0)) { + return switch (mediaTypes.getFirst()) { case banner -> resolveFormatFromBannerImp(imp); case video -> resolveFormatFromVideoImp(imp); default -> null; @@ -346,7 +353,7 @@ private static Format resolveFormatFromBannerImp(Imp imp) { case 0 -> formatOf( ObjectUtil.getIfNotNull(banner, Banner::getW), ObjectUtil.getIfNotNull(banner, Banner::getH)); - case 1 -> formats.get(0); + case 1 -> formats.getFirst(); default -> null; }; } diff --git a/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java new file mode 100644 index 00000000000..07c05134877 --- /dev/null +++ b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java @@ -0,0 +1,79 @@ +package org.prebid.server.floors; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorEnforcement; +import org.prebid.server.floors.model.PriceFloorModelGroup; +import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; + +import java.util.List; +import java.util.Optional; + +public class NoSignalBidderPriceFloorAdjuster implements PriceFloorAdjuster { + + private static final String ALL_BIDDERS = "*"; + + private final PriceFloorAdjuster delegate; + + public NoSignalBidderPriceFloorAdjuster(PriceFloorAdjuster delegate) { + this.delegate = delegate; + } + + @Override + public Price adjustForImp(Imp imp, + String bidder, + BidRequest bidRequest, + Account account, + List debugWarnings) { + + final Optional optionalFloors = Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getFloors); + + final Boolean shouldSkip = optionalFloors + .map(floors -> BooleanUtils.isFalse(floors.getEnabled()) || BooleanUtils.isTrue(floors.getSkipped())) + .orElse(false); + + if (shouldSkip) { + return delegate.adjustForImp(imp, bidder, bidRequest, account, debugWarnings); + } + + return optionalFloors + .map(PriceFloorRules::getData) + .map(PriceFloorData::getModelGroups) + .filter(CollectionUtils::isNotEmpty) + .map(modelGroups -> modelGroups.get(0)) + .map(PriceFloorModelGroup::getNoFloorSignalBidders) + .or(() -> optionalFloors + .map(PriceFloorRules::getData) + .map(PriceFloorData::getNoFloorSignalBidders)) + .or(() -> optionalFloors + .map(PriceFloorRules::getEnforcement) + .map(PriceFloorEnforcement::getNoFloorSignalBidders)) + .filter(noSignalBidders -> isNoSignalBidder(bidder, noSignalBidders)) + .map(ignored -> { + debugWarnings.add("noFloorSignal to bidder " + bidder); + return Price.empty(); + }) + .orElseGet(() -> delegate.adjustForImp(imp, bidder, bidRequest, account, debugWarnings)); + } + + @Override + public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { + return delegate.revertAdjustmentForImp(imp, bidder, bidRequest, account); + } + + private static boolean isNoSignalBidder(String bidder, List noSignalBidders) { + return noSignalBidders.stream().anyMatch(noSignalBidder -> StringUtils.equalsIgnoreCase(noSignalBidder, bidder)) + || noSignalBidders.contains(ALL_BIDDERS); + } +} diff --git a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java index b1ca2a78441..89a2ac7bc24 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java @@ -2,14 +2,17 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import org.prebid.server.bidder.model.Price; import org.prebid.server.settings.model.Account; import org.prebid.server.util.ObjectUtil; -import java.math.BigDecimal; +import java.util.List; public interface PriceFloorAdjuster { - BigDecimal adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account); + Price adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account, List debugWarnings); + + Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account); static NoOpPriceFloorAdjuster noOp() { return new NoOpPriceFloorAdjuster(); @@ -18,8 +21,18 @@ static NoOpPriceFloorAdjuster noOp() { class NoOpPriceFloorAdjuster implements PriceFloorAdjuster { @Override - public BigDecimal adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return ObjectUtil.getIfNotNull(imp, Imp::getBidfloor); + public Price adjustForImp(Imp imp, + String bidder, + BidRequest bidRequest, + Account account, + List debugWarnings) { + + return ObjectUtil.getIfNotNull(imp, i -> Price.of(i.getBidfloorcur(), i.getBidfloor())); + } + + @Override + public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { + return ObjectUtil.getIfNotNull(imp, i -> Price.of(i.getBidfloorcur(), i.getBidfloor())); } } } diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index fd4e4ef9ab5..0692dce090a 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -6,8 +6,6 @@ import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.impl.ConcurrentHashSet; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import lombok.Value; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -22,6 +20,8 @@ import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.ApplicationSettings; @@ -31,8 +31,8 @@ import org.prebid.server.settings.model.AccountPriceFloorsFetchConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.Map; import java.util.Objects; @@ -137,13 +137,13 @@ private static AccountPriceFloorsFetchConfig getFetchConfig(Account account) { } private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) { - final Long accountTimeout = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getTimeout); + final Long accountTimeout = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getTimeoutMs); final Long timeout = ObjectUtils.firstNonNull( ObjectUtil.getIfNotNull(debugProperties, PriceFloorDebugProperties::getMinTimeoutMs), ObjectUtil.getIfNotNull(debugProperties, PriceFloorDebugProperties::getMaxTimeoutMs), accountTimeout); final Long maxFetchFileSizeKb = - ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getMaxFileSize); + ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getMaxFileSizeKb); final String fetchUrl = fetchConfig.getUrl(); fetchInProgress.add(accountId); @@ -310,11 +310,8 @@ private void periodicFetch(String accountId) { } private Future accountById(String accountId) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture() - : applicationSettings - .getAccountById(accountId, timeoutFactory.create(ACCOUNT_FETCH_TIMEOUT_MS)) - .recover(ignored -> Future.succeededFuture()); + return applicationSettings.getAccountById(accountId, timeoutFactory.create(ACCOUNT_FETCH_TIMEOUT_MS)) + .otherwiseEmpty(); } @Value(staticConstructor = "of") diff --git a/src/main/java/org/prebid/server/floors/PriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/PriceFloorProcessor.java index 9a54409f4c2..ea698b7b54d 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorProcessor.java @@ -1,10 +1,17 @@ package org.prebid.server.floors; -import org.prebid.server.auction.model.AuctionContext; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.settings.model.Account; + +import java.util.List; public interface PriceFloorProcessor { - AuctionContext enrichWithPriceFloors(AuctionContext auctionContext); + BidRequest enrichWithPriceFloors(BidRequest bidRequest, + Account account, + String bidder, + List errors, + List warnings); static NoOpPriceFloorProcessor noOp() { return new NoOpPriceFloorProcessor(); @@ -13,8 +20,13 @@ static NoOpPriceFloorProcessor noOp() { class NoOpPriceFloorProcessor implements PriceFloorProcessor { @Override - public AuctionContext enrichWithPriceFloors(AuctionContext auctionContext) { - return auctionContext; + public BidRequest enrichWithPriceFloors(BidRequest bidRequest, + Account account, + String bidder, + List errors, + List warnings) { + + return bidRequest; } } } diff --git a/src/main/java/org/prebid/server/floors/PriceFloorResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorResolver.java index 3b596027a9f..8752f81468c 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorResolver.java @@ -16,14 +16,16 @@ PriceFloorResult resolve(BidRequest bidRequest, Imp imp, ImpMediaType mediaType, Format format, + String bidder, List warnings); default PriceFloorResult resolve(BidRequest bidRequest, PriceFloorRules floorRules, Imp imp, + String bidder, List warnings) { - return resolve(bidRequest, floorRules, imp, null, null, warnings); + return resolve(bidRequest, floorRules, imp, null, null, bidder, warnings); } static NoOpPriceFloorResolver noOp() { @@ -38,6 +40,7 @@ public PriceFloorResult resolve(BidRequest bidRequest, Imp imp, ImpMediaType mediaType, Format format, + String bidder, List warnings) { return null; diff --git a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java index d5bbde4b476..73eb62cfe84 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java @@ -1,11 +1,11 @@ package org.prebid.server.floors; -import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.EnrichingApplicationSettings; @@ -35,26 +35,16 @@ public class PriceFloorsConfigResolver { private static final int MAX_ENFORCE_RATE_VALUE = 100; private static final long DEFAULT_MAX_AGE_SEC_VALUE = 86400L; - private final Account defaultAccount; private final Metrics metrics; - private final AccountPriceFloorsConfig defaultFloorsConfig; - public PriceFloorsConfigResolver(Account defaultAccount, Metrics metrics) { - this.defaultAccount = Objects.requireNonNull(defaultAccount); - this.defaultFloorsConfig = getFloorsConfig(defaultAccount); + public PriceFloorsConfigResolver(Metrics metrics) { this.metrics = Objects.requireNonNull(metrics); } - private static AccountPriceFloorsConfig getFloorsConfig(Account account) { - final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); - - return ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); - } - - public Future updateFloorsConfig(Account account) { + public Account resolve(Account account, AccountPriceFloorsConfig fallbackPriceFloorConfig) { try { - validatePriceFloorConfig(account, defaultFloorsConfig); - return Future.succeededFuture(account); + validatePriceFloorConfig(account); + return account; } catch (PreBidException e) { final String message = "Account with id '%s' has invalid config: %s" .formatted(account.getId(), e.getMessage()); @@ -65,75 +55,60 @@ public Future updateFloorsConfig(Account account) { conditionalLogger.error(message, 0.01d); } - return Future.succeededFuture(fallbackToDefaultConfig(account)); + return account.toBuilder() + .auction(account.getAuction().toBuilder().priceFloors(fallbackPriceFloorConfig).build()) + .build(); } - private static void validatePriceFloorConfig(Account account, AccountPriceFloorsConfig defaultFloorsConfig) { + private static void validatePriceFloorConfig(Account account) { final AccountPriceFloorsConfig floorsConfig = getFloorsConfig(account); if (floorsConfig == null) { return; } - final Integer accountEnforceRate = floorsConfig.getEnforceFloorsRate(); - final Integer enforceFloorsRate = accountEnforceRate != null - ? accountEnforceRate - : ObjectUtil.getIfNotNull(defaultFloorsConfig, AccountPriceFloorsConfig::getEnforceFloorsRate); - if (enforceFloorsRate != null - && isNotInRange(enforceFloorsRate, MIN_ENFORCE_RATE_VALUE, MAX_ENFORCE_RATE_VALUE)) { - throw new PreBidException( - invalidPriceFloorsPropertyMessage("enforce-floors-rate", enforceFloorsRate)); + + final Integer enforceRate = floorsConfig.getEnforceFloorsRate(); + if (enforceRate != null && isNotInRange(enforceRate, MIN_ENFORCE_RATE_VALUE, MAX_ENFORCE_RATE_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("enforce-floors-rate", enforceRate)); } + final AccountPriceFloorsFetchConfig fetchConfig = ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getFetch); - final AccountPriceFloorsFetchConfig defaultFetchConfig = - ObjectUtil.getIfNotNull(defaultFloorsConfig, AccountPriceFloorsConfig::getFetch); - validatePriceFloorsFetchConfig(fetchConfig, defaultFetchConfig); + validatePriceFloorsFetchConfig(fetchConfig); } - private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig fetchConfig, - AccountPriceFloorsFetchConfig defaultFetchConfig) { + private static AccountPriceFloorsConfig getFloorsConfig(Account account) { + final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); + + return ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); + } + + private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig fetchConfig) { if (fetchConfig == null) { return; } - final Long accountMaxAgeSec = fetchConfig.getMaxAgeSec(); - final Long defaultMaxAgeSec = - ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getMaxAgeSec); - final long maxAgeSec = accountMaxAgeSec != null - ? accountMaxAgeSec - : defaultMaxAgeSec != null ? defaultMaxAgeSec : DEFAULT_MAX_AGE_SEC_VALUE; + final long maxAgeSec = ObjectUtils.defaultIfNull(fetchConfig.getMaxAgeSec(), DEFAULT_MAX_AGE_SEC_VALUE); if (isNotInRange(maxAgeSec, MIN_MAX_AGE_SEC_VALUE, MAX_AGE_SEC_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-age-sec", maxAgeSec)); } - final Long accountPeriodicSec = fetchConfig.getPeriodSec(); - final Long periodicSec = accountPeriodicSec != null - ? accountPeriodicSec - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec); + final Long periodicSec = fetchConfig.getPeriodSec(); if (periodicSec != null && isNotInRange(periodicSec, MIN_PERIODIC_SEC_VALUE, maxAgeSec)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("period-sec", periodicSec)); } - final Long accountTimeout = fetchConfig.getTimeout(); - final Long timeout = accountTimeout != null - ? accountTimeout - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getTimeout); + final Long timeout = fetchConfig.getTimeoutMs(); if (timeout != null && isNotInRange(timeout, MIN_TIMEOUT_MS_VALUE, MAX_TIMEOUT_MS_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("timeout-ms", timeout)); } - final Long accountMaxRules = fetchConfig.getMaxRules(); - final Long maxRules = accountMaxRules != null - ? accountMaxRules - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getMaxRules); + final Long maxRules = fetchConfig.getMaxRules(); if (maxRules != null && isNotInRange(maxRules, MIN_RULES_VALUE, MAX_RULES_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); } - final Long accountMaxFileSize = fetchConfig.getMaxFileSize(); - final Long maxFileSize = accountMaxFileSize != null - ? accountMaxFileSize - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getMaxFileSize); + final Long maxFileSize = fetchConfig.getMaxFileSizeKb(); if (maxFileSize != null && isNotInRange(maxFileSize, MIN_FILE_SIZE_VALUE, MAX_FILE_SIZE_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-file-size-kb", maxFileSize)); } @@ -146,14 +121,4 @@ private static boolean isNotInRange(long number, long min, long max) { private static String invalidPriceFloorsPropertyMessage(String property, Object value) { return "Invalid price-floors property '%s', value passed: %s".formatted(property, value); } - - private Account fallbackToDefaultConfig(Account account) { - final AccountAuctionConfig auctionConfig = account.getAuction(); - final AccountPriceFloorsConfig defaultPriceFloorsConfig = - ObjectUtil.getIfNotNull(defaultAccount.getAuction(), AccountAuctionConfig::getPriceFloors); - - return account.toBuilder() - .auction(auctionConfig.toBuilder().priceFloors(defaultPriceFloorsConfig).build()) - .build(); - } } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java index 79f3b485584..bdca465af36 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java @@ -26,4 +26,7 @@ public class PriceFloorData { @JsonProperty("modelGroups") List modelGroups; + + @JsonProperty("noFloorSignalBidders") + List noFloorSignalBidders; } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorDebugProperties.java b/src/main/java/org/prebid/server/floors/model/PriceFloorDebugProperties.java index 77c2e1c0cab..c75af39004d 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorDebugProperties.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorDebugProperties.java @@ -4,7 +4,7 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Validated @Data diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorEnforcement.java b/src/main/java/org/prebid/server/floors/model/PriceFloorEnforcement.java index b9aa9d7e610..331072b0b26 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorEnforcement.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorEnforcement.java @@ -4,6 +4,8 @@ import lombok.Builder; import lombok.Value; +import java.util.List; + @Value @Builder(toBuilder = true) public class PriceFloorEnforcement { @@ -26,4 +28,7 @@ public class PriceFloorEnforcement { @JsonProperty("enforceRate") Integer enforceRate; + + @JsonProperty("noFloorSignalBidders") + List noFloorSignalBidders; } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorField.java b/src/main/java/org/prebid/server/floors/model/PriceFloorField.java index fcdccb19ab8..86a8fec7423 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorField.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorField.java @@ -2,5 +2,5 @@ public enum PriceFloorField { - siteDomain, pubDomain, domain, bundle, channel, mediaType, size, gptSlot, adUnitCode, country, deviceType + siteDomain, pubDomain, domain, bundle, channel, mediaType, size, gptSlot, adUnitCode, country, deviceType, bidder } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorModelGroup.java b/src/main/java/org/prebid/server/floors/model/PriceFloorModelGroup.java index ebc8c6eb72a..0c2cb3fe5fc 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorModelGroup.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorModelGroup.java @@ -6,6 +6,7 @@ import lombok.Value; import java.math.BigDecimal; +import java.util.List; import java.util.Map; @Value @@ -30,4 +31,7 @@ public class PriceFloorModelGroup { @JsonProperty("default") BigDecimal defaultFloor; + + @JsonProperty("noFloorSignalBidders") + List noFloorSignalBidders; } diff --git a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java index f7a2c6ca6b9..791a1da06c1 100755 --- a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java @@ -2,11 +2,11 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.execution.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.vertx.CircuitBreaker; diff --git a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java index b258f14a531..2cea7119714 100644 --- a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java @@ -101,7 +101,7 @@ private static String resolveCountry(CityResponse cityResponse) { private static String resolveRegion(CityResponse cityResponse) { final List subdivisions = cityResponse != null ? cityResponse.getSubdivisions() : null; - final Subdivision firstSubdivision = CollectionUtils.isEmpty(subdivisions) ? null : subdivisions.get(0); + final Subdivision firstSubdivision = CollectionUtils.isEmpty(subdivisions) ? null : subdivisions.getFirst(); return firstSubdivision != null ? firstSubdivision.getIsoCode() : null; } diff --git a/src/main/java/org/prebid/server/handler/BidderParamHandler.java b/src/main/java/org/prebid/server/handler/BidderParamHandler.java index 26366130cd1..f27137a2e1e 100644 --- a/src/main/java/org/prebid/server/handler/BidderParamHandler.java +++ b/src/main/java/org/prebid/server/handler/BidderParamHandler.java @@ -1,14 +1,18 @@ package org.prebid.server.handler; -import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.prebid.server.model.Endpoint; import org.prebid.server.util.HttpUtil; import org.prebid.server.validation.BidderParamValidator; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; +import java.util.Collections; +import java.util.List; import java.util.Objects; -public class BidderParamHandler implements Handler { +public class BidderParamHandler implements ApplicationResource { private final BidderParamValidator bidderParamValidator; @@ -16,6 +20,11 @@ public BidderParamHandler(BidderParamValidator bidderParamValidator) { this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.bidder_params.value())); + } + @Override public void handle(RoutingContext routingContext) { HttpUtil.executeSafely(routingContext, Endpoint.bidder_params, diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 2009743ec8f..746dffb5fc7 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -3,13 +3,10 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.analytics.model.CookieSyncEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; @@ -32,6 +29,8 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.model.Endpoint; import org.prebid.server.privacy.gdpr.model.TcfContext; @@ -41,12 +40,16 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Optional; -public class CookieSyncHandler implements Handler { +public class CookieSyncHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(CookieSyncHandler.class); private static final ConditionalLogger BAD_REQUEST_LOGGER = new ConditionalLogger(logger); @@ -94,6 +97,11 @@ public CookieSyncHandler(long defaultTimeout, this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.POST, Endpoint.cookie_sync.value())); + } + @Override public void handle(RoutingContext routingContext) { metrics.updateCookieSyncRequestMetric(); @@ -158,10 +166,7 @@ private Future fillWithAccount(CookieSyncContext cookieSyncCo } private Future accountById(String accountId, Timeout timeout) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture(Account.empty(accountId)) - : applicationSettings.getAccountById(accountId, timeout) - .otherwise(Account.empty(accountId)); + return applicationSettings.getAccountById(accountId, timeout).otherwise(Account.empty(accountId)); } private CookieSyncContext fillWithGppContext(CookieSyncContext cookieSyncContext) { @@ -227,27 +232,32 @@ private void respondWithError(Throwable error, RoutingContext routingContext) { final HttpResponseStatus status; final String body; - if (error instanceof InvalidCookieSyncRequestException) { - status = HttpResponseStatus.BAD_REQUEST; - body = "Invalid request format: " + message; - - metrics.updateUserSyncBadRequestMetric(); - BAD_REQUEST_LOGGER.info(message, logSamplingRate); - } else if (error instanceof UnauthorizedUidsException) { - status = HttpResponseStatus.UNAUTHORIZED; - body = "Unauthorized: " + message; - - metrics.updateUserSyncOptoutMetric(); - } else if (error instanceof InvalidAccountConfigException) { - status = HttpResponseStatus.BAD_REQUEST; - body = "Invalid account configuration: " + message; - - BAD_REQUEST_LOGGER.info(message, logSamplingRate); - } else { - status = HttpResponseStatus.INTERNAL_SERVER_ERROR; - body = "Unexpected setuid processing error: " + message; - - logger.warn(body, error); + switch (error) { + case InvalidCookieSyncRequestException invalidCookieSyncRequestException -> { + status = HttpResponseStatus.BAD_REQUEST; + body = "Invalid request format: " + message; + + metrics.updateUserSyncBadRequestMetric(); + BAD_REQUEST_LOGGER.info(message, logSamplingRate); + } + case UnauthorizedUidsException unauthorizedUidsException -> { + status = HttpResponseStatus.UNAUTHORIZED; + body = "Unauthorized: " + message; + + metrics.updateUserSyncOptoutMetric(); + } + case InvalidAccountConfigException invalidAccountConfigException -> { + status = HttpResponseStatus.BAD_REQUEST; + body = "Invalid account configuration: " + message; + + BAD_REQUEST_LOGGER.info(message, logSamplingRate); + } + default -> { + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + body = "Unexpected setuid processing error: " + message; + + logger.warn(body, error); + } } HttpUtil.executeSafely(routingContext, Endpoint.cookie_sync, diff --git a/src/main/java/org/prebid/server/handler/CustomizedAdminEndpoint.java b/src/main/java/org/prebid/server/handler/CustomizedAdminEndpoint.java deleted file mode 100644 index 4ac49624277..00000000000 --- a/src/main/java/org/prebid/server/handler/CustomizedAdminEndpoint.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.prebid.server.handler; - -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.ext.auth.AuthProvider; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BasicAuthHandler; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; - -import java.util.Map; -import java.util.Objects; - -public class CustomizedAdminEndpoint { - - private final String path; - private final Handler handler; - private final boolean isOnApplicationPort; - private final boolean isProtected; - private Map credentials; - - public CustomizedAdminEndpoint(String path, Handler handler, boolean isOnApplicationPort, - boolean isProtected) { - this.path = Objects.requireNonNull(path); - this.handler = Objects.requireNonNull(handler); - this.isOnApplicationPort = isOnApplicationPort; - this.isProtected = isProtected; - } - - public CustomizedAdminEndpoint withCredentials(Map credentials) { - this.credentials = credentials; - return this; - } - - public boolean isOnApplicationPort() { - return isOnApplicationPort; - } - - public void router(Router router) { - if (isProtected) { - routeToHandlerWithCredentials(router); - } else { - routeToHandler(router); - } - } - - private void routeToHandlerWithCredentials(Router router) { - if (credentials == null) { - throw new IllegalArgumentException("Credentials for admin endpoint is empty."); - } - - final AuthProvider authProvider = createAuthProvider(credentials); - router.route(path).handler(BasicAuthHandler.create(authProvider)).handler(handler); - } - - private void routeToHandler(Router router) { - router.route(path).handler(handler); - } - - private AuthProvider createAuthProvider(Map credentials) { - return (authInfo, resultHandler) -> { - if (MapUtils.isEmpty(credentials)) { - resultHandler.handle(Future.failedFuture("Credentials not set in configuration.")); - return; - } - - final String requestUsername = authInfo.getString("username"); - final String requestPassword = StringUtils.chomp(authInfo.getString("password")); - - final String storedPassword = credentials.get(requestUsername); - if (StringUtils.isNotBlank(requestPassword) && Objects.equals(storedPassword, requestPassword)) { - resultHandler.handle(Future.succeededFuture()); - } else { - resultHandler.handle(Future.failedFuture("No such user, or password incorrect.")); - } - }; - } -} diff --git a/src/main/java/org/prebid/server/handler/DealsStatusHandler.java b/src/main/java/org/prebid/server/handler/DealsStatusHandler.java deleted file mode 100644 index 15a9dc066dd..00000000000 --- a/src/main/java/org/prebid/server/handler/DealsStatusHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.prebid.server.handler; - -import io.netty.handler.codec.http.HttpHeaderValues; -import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.proto.report.DeliveryProgressReport; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.HttpUtil; - -import java.util.Objects; - -public class DealsStatusHandler implements Handler { - - private static final Logger logger = LoggerFactory.getLogger(DealsStatusHandler.class); - - private final DeliveryProgressService deliveryProgressService; - private final JacksonMapper mapper; - - public DealsStatusHandler(DeliveryProgressService deliveryProgressService, JacksonMapper mapper) { - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public void handle(RoutingContext routingContext) { - final DeliveryProgressReport deliveryProgressReport = deliveryProgressService - .getOverallDeliveryProgressReport(); - final String body = mapper.encodeToString(deliveryProgressReport); - - // don't send the response if client has gone - if (routingContext.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; - } - - routingContext.response() - .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .exceptionHandler(this::handleResponseException) - .end(body); - } - - private void handleResponseException(Throwable throwable) { - logger.warn("Failed to send deals status response: {0}", throwable.getMessage()); - } -} diff --git a/src/main/java/org/prebid/server/handler/ExceptionHandler.java b/src/main/java/org/prebid/server/handler/ExceptionHandler.java index 952d46dd47e..f56a21d8e5c 100644 --- a/src/main/java/org/prebid/server/handler/ExceptionHandler.java +++ b/src/main/java/org/prebid/server/handler/ExceptionHandler.java @@ -1,9 +1,9 @@ package org.prebid.server.handler; import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import java.io.IOException; @@ -26,7 +26,7 @@ public static ExceptionHandler create(Metrics metrics) { @Override public void handle(Throwable exception) { if (shouldLogException(exception)) { - logger.warn("Generic error handler: {0}, cause: {1}", + logger.warn("Generic error handler: {}, cause: {}", errorMessageFrom(exception), errorMessageFrom(exception.getCause())); } metrics.updateConnectionAcceptErrors(); diff --git a/src/main/java/org/prebid/server/handler/ForceDealsUpdateHandler.java b/src/main/java/org/prebid/server/handler/ForceDealsUpdateHandler.java deleted file mode 100644 index 942b4953147..00000000000 --- a/src/main/java/org/prebid/server/handler/ForceDealsUpdateHandler.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.prebid.server.handler; - -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.deals.AlertHttpService; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.DeliveryStatsService; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.PlannerService; -import org.prebid.server.deals.RegisterService; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.util.HttpUtil; - -import java.time.ZonedDateTime; -import java.util.Objects; - -public class ForceDealsUpdateHandler implements Handler { - - private static final String ACTION_NAME_PARAM = "action_name"; - - private final DeliveryStatsService deliveryStatsService; - private final PlannerService plannerService; - private final RegisterService registerService; - private final AlertHttpService alertHttpService; - private final DeliveryProgressService deliveryProgressService; - private final LineItemService lineItemService; - private final String endpoint; - - public ForceDealsUpdateHandler(DeliveryStatsService deliveryStatsService, - PlannerService plannerService, - RegisterService registerService, - AlertHttpService alertHttpService, - DeliveryProgressService deliveryProgressService, - LineItemService lineItemService, - String endpoint) { - - this.deliveryStatsService = Objects.requireNonNull(deliveryStatsService); - this.plannerService = Objects.requireNonNull(plannerService); - this.registerService = Objects.requireNonNull(registerService); - this.alertHttpService = Objects.requireNonNull(alertHttpService); - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.lineItemService = Objects.requireNonNull(lineItemService); - this.endpoint = Objects.requireNonNull(endpoint); - } - - @Override - public void handle(RoutingContext routingContext) { - try { - handleDealsAction(dealsActionFrom(routingContext)); - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.NO_CONTENT.code()) - .end()); - } catch (InvalidRequestException e) { - respondWithError(routingContext, HttpResponseStatus.BAD_REQUEST, e); - } catch (Exception e) { - respondWithError(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, e); - } - } - - private static DealsAction dealsActionFrom(RoutingContext routingContext) { - final String actionName = routingContext.request().getParam(ACTION_NAME_PARAM); - if (StringUtils.isEmpty(actionName)) { - throw new InvalidRequestException("Parameter '%s' is required and can't be empty" - .formatted(ACTION_NAME_PARAM)); - } - - try { - return DealsAction.valueOf(actionName.toUpperCase()); - } catch (IllegalArgumentException ignored) { - throw new InvalidRequestException("Given '%s' parameter value '%s' is not among possible actions" - .formatted(ACTION_NAME_PARAM, actionName)); - } - } - - private void handleDealsAction(DealsAction dealsAction) { - switch (dealsAction) { - case UPDATE_LINE_ITEMS -> plannerService.updateLineItemMetaData(); - case SEND_REPORT -> deliveryStatsService.sendDeliveryProgressReports(); - case REGISTER_INSTANCE -> registerService.performRegistration(); - case RESET_ALERT_COUNT -> { - alertHttpService.resetAlertCount("pbs-register-client-error"); - alertHttpService.resetAlertCount("pbs-planner-client-error"); - alertHttpService.resetAlertCount("pbs-planner-empty-response-error"); - alertHttpService.resetAlertCount("pbs-delivery-stats-client-error"); - } - case CREATE_REPORT -> deliveryProgressService.createDeliveryProgressReports(ZonedDateTime.now()); - case INVALIDATE_LINE_ITEMS -> lineItemService.invalidateLineItems(); - } - } - - private void respondWithError(RoutingContext routingContext, HttpResponseStatus statusCode, Exception exception) { - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(statusCode.code()) - .end(exception.getMessage())); - } - - enum DealsAction { - UPDATE_LINE_ITEMS, SEND_REPORT, REGISTER_INSTANCE, RESET_ALERT_COUNT, CREATE_REPORT, INVALIDATE_LINE_ITEMS - } -} diff --git a/src/main/java/org/prebid/server/handler/GetuidsHandler.java b/src/main/java/org/prebid/server/handler/GetuidsHandler.java index deb3822e4a9..705c5f94e27 100644 --- a/src/main/java/org/prebid/server/handler/GetuidsHandler.java +++ b/src/main/java/org/prebid/server/handler/GetuidsHandler.java @@ -1,7 +1,7 @@ package org.prebid.server.handler; import com.fasterxml.jackson.annotation.JsonInclude; -import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; import lombok.Value; @@ -10,12 +10,16 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.Endpoint; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -public class GetuidsHandler implements Handler { +public class GetuidsHandler implements ApplicationResource { private final UidsCookieService uidsCookieService; private final JacksonMapper mapper; @@ -25,6 +29,11 @@ public GetuidsHandler(UidsCookieService uidsCookieService, JacksonMapper mapper) this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.getuids.value())); + } + @Override public void handle(RoutingContext routingContext) { final Map uids = uidsFrom(routingContext); diff --git a/src/main/java/org/prebid/server/handler/LineItemStatusHandler.java b/src/main/java/org/prebid/server/handler/LineItemStatusHandler.java deleted file mode 100644 index e43a543025a..00000000000 --- a/src/main/java/org/prebid/server/handler/LineItemStatusHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.prebid.server.handler; - -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.proto.report.LineItemStatusReport; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.HttpUtil; - -import java.util.Objects; - -public class LineItemStatusHandler implements Handler { - - private static final Logger logger = LoggerFactory.getLogger(LineItemStatusHandler.class); - - private static final String ID_PARAM = "id"; - - private final DeliveryProgressService deliveryProgressService; - private final JacksonMapper mapper; - private final String endpoint; - - public LineItemStatusHandler(DeliveryProgressService deliveryProgressService, JacksonMapper mapper, - String endpoint) { - this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); - this.mapper = Objects.requireNonNull(mapper); - this.endpoint = Objects.requireNonNull(endpoint); - } - - @Override - public void handle(RoutingContext routingContext) { - routingContext.response() - .exceptionHandler(LineItemStatusHandler::handleResponseException); - - final String lineItemId = lineItemIdFrom(routingContext); - if (StringUtils.isEmpty(lineItemId)) { - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) - .end(ID_PARAM + " parameter is required")); - return; - } - - try { - final LineItemStatusReport report = deliveryProgressService.getLineItemStatusReport(lineItemId); - - HttpUtil.headers().forEach(entry -> routingContext.response().putHeader(entry.getKey(), entry.getValue())); - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.OK.code()) - .end(mapper.encodeToString(report))); - } catch (PreBidException e) { - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) - .end(e.getMessage())); - } catch (Exception e) { - HttpUtil.executeSafely(routingContext, endpoint, - response -> response - .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) - .end(e.getMessage())); - } - } - - private static String lineItemIdFrom(RoutingContext routingContext) { - return routingContext.request().getParam(ID_PARAM); - } - - private static void handleResponseException(Throwable exception) { - logger.warn("Failed to send line item status response: {0}", exception.getMessage()); - } -} diff --git a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java index 383b0cf7455..971cb82204f 100644 --- a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java +++ b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java @@ -3,14 +3,11 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; @@ -18,13 +15,12 @@ import org.prebid.server.analytics.model.NotificationEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.gpp.model.GppContextCreator; -import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.deals.UserService; -import org.prebid.server.deals.events.ApplicationEventService; import org.prebid.server.events.EventRequest; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.settings.ApplicationSettings; @@ -33,51 +29,43 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ResourceUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.io.IOException; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** * Accepts notifications from browsers and mobile application for further processing by {@link AnalyticsReporter} * and responding with tracking pixel when requested. */ -public class NotificationEventHandler implements Handler { +public class NotificationEventHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(NotificationEventHandler.class); private static final String TRACKING_PIXEL_PNG = "static/tracking-pixel.png"; private static final String PNG_CONTENT_TYPE = "image/png"; - private final UidsCookieService uidsCookieService; - private final ApplicationEventService applicationEventService; - private final UserService userService; private final ActivityInfrastructureCreator activityInfrastructureCreator; private final AnalyticsReporterDelegator analyticsDelegator; private final TimeoutFactory timeoutFactory; private final ApplicationSettings applicationSettings; private final long defaultTimeoutMillis; - private final boolean dealsEnabled; private final TrackingPixel trackingPixel; - public NotificationEventHandler(UidsCookieService uidsCookieService, - ApplicationEventService applicationEventService, - UserService userService, - ActivityInfrastructureCreator activityInfrastructureCreator, + public NotificationEventHandler(ActivityInfrastructureCreator activityInfrastructureCreator, AnalyticsReporterDelegator analyticsDelegator, TimeoutFactory timeoutFactory, ApplicationSettings applicationSettings, - long defaultTimeoutMillis, - boolean dealsEnabled) { + long defaultTimeoutMillis) { - this.uidsCookieService = Objects.requireNonNull(uidsCookieService); - this.applicationEventService = applicationEventService; - this.userService = userService; this.activityInfrastructureCreator = Objects.requireNonNull(activityInfrastructureCreator); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.applicationSettings = Objects.requireNonNull(applicationSettings); this.defaultTimeoutMillis = defaultTimeoutMillis; - this.dealsEnabled = dealsEnabled; trackingPixel = createTrackingPixel(); } @@ -93,6 +81,11 @@ private static TrackingPixel createTrackingPixel() { return TrackingPixel.of(PNG_CONTENT_TYPE, bytes); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.event.value())); + } + @Override public void handle(RoutingContext routingContext) { try { @@ -144,45 +137,35 @@ private static Future handleAccountExceptionOrFallback(Throwable except private void handleEvent(AsyncResult async, EventRequest eventRequest, RoutingContext routingContext) { if (async.failed()) { - respondWithServerError(routingContext, "Error occurred while fetching account", async.cause()); - } else { - final Account account = async.result(); - - final String lineItemId = eventRequest.getLineItemId(); - final String bidId = eventRequest.getBidId(); - if (dealsEnabled && lineItemId != null) { - applicationEventService.publishLineItemWinEvent(lineItemId); - userService.processWinEvent(lineItemId, bidId, uidsCookieService.parseFromRequest(routingContext)); - } - - final boolean eventsEnabledForAccount = Objects.equals(accountEventsEnabled(account), true); - final boolean eventsEnabledForRequest = eventRequest.getAnalytics() == EventRequest.Analytics.enabled; - - if (!eventsEnabledForAccount && eventsEnabledForRequest) { - respondWithUnauthorized(routingContext, - "Account '%s' doesn't support events".formatted(account.getId())); - return; - } - - final EventRequest.Type eventType = eventRequest.getType(); - if (eventsEnabledForRequest) { - final NotificationEvent notificationEvent = NotificationEvent.builder() - .type(eventType == EventRequest.Type.win - ? NotificationEvent.Type.win : NotificationEvent.Type.imp) - .bidId(eventRequest.getBidId()) - .account(account) - .bidder(eventRequest.getBidder()) - .timestamp(eventRequest.getTimestamp()) - .integration(eventRequest.getIntegration()) - .httpContext(HttpRequestContext.from(routingContext)) - .lineItemId(lineItemId) - .activityInfrastructure(activityInfrastructure(account)) - .build(); - - analyticsDelegator.processEvent(notificationEvent); - } - respondWithOk(routingContext, eventRequest.getFormat() == EventRequest.Format.image); + respondWithAccountError(routingContext, async.cause()); + return; + } + + final Account account = async.result(); + final boolean eventsEnabledForAccount = Objects.equals(accountEventsEnabled(account), true); + final boolean eventsEnabledForRequest = eventRequest.getAnalytics() == EventRequest.Analytics.enabled; + + if (!eventsEnabledForAccount && eventsEnabledForRequest) { + respondWithUnauthorized(routingContext, "Account '%s' doesn't support events".formatted(account.getId())); + return; + } + + final EventRequest.Type eventType = eventRequest.getType(); + if (eventsEnabledForRequest) { + final NotificationEvent notificationEvent = NotificationEvent.builder() + .type(eventType == EventRequest.Type.win ? NotificationEvent.Type.win : NotificationEvent.Type.imp) + .bidId(eventRequest.getBidId()) + .account(account) + .bidder(eventRequest.getBidder()) + .timestamp(eventRequest.getTimestamp()) + .integration(eventRequest.getIntegration()) + .httpContext(HttpRequestContext.from(routingContext)) + .activityInfrastructure(activityInfrastructure(account)) + .build(); + + analyticsDelegator.processEvent(notificationEvent); } + respondWithOk(routingContext, eventRequest.getFormat() == EventRequest.Format.image); } private static Boolean accountEventsEnabled(Account account) { @@ -202,13 +185,14 @@ private ActivityInfrastructure activityInfrastructure(Account account) { private void respondWithOk(RoutingContext routingContext, boolean respondWithPixel) { if (respondWithPixel) { - HttpUtil.executeSafely(routingContext, Endpoint.event, + HttpUtil.executeSafely( + routingContext, + Endpoint.event, response -> response .putHeader(HttpHeaders.CONTENT_TYPE, trackingPixel.getContentType()) .end(Buffer.buffer(trackingPixel.getContent()))); } else { - HttpUtil.executeSafely(routingContext, Endpoint.event, - HttpServerResponse::end); + HttpUtil.executeSafely(routingContext, Endpoint.event, HttpServerResponse::end); } } @@ -220,14 +204,16 @@ private static void respondWithUnauthorized(RoutingContext routingContext, Strin respondWith(routingContext, HttpResponseStatus.UNAUTHORIZED, message); } - private static void respondWithServerError(RoutingContext routingContext, String message, Throwable exception) { - logger.warn(message, exception); - final String body = "%s: %s".formatted(message, exception.getMessage()); + private static void respondWithAccountError(RoutingContext routingContext, Throwable exception) { + logger.warn("Error occurred while fetching account", exception); + final String body = "Error occurred while fetching account: " + exception.getMessage(); respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, body); } private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { - HttpUtil.executeSafely(routingContext, Endpoint.event, + HttpUtil.executeSafely( + routingContext, + Endpoint.event, response -> response .setStatusCode(status.code()) .end(body)); @@ -236,8 +222,7 @@ private static void respondWith(RoutingContext routingContext, HttpResponseStatu /** * Internal class for holding pixels content type to its value. */ - @AllArgsConstructor(staticName = "of") - @Value + @Value(staticConstructor = "of") private static class TrackingPixel { String contentType; diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index c4360fb003a..48551d37b03 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -2,24 +2,27 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; -import io.vertx.core.Handler; import io.vertx.core.http.Cookie; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.StringUtils; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.Endpoint; import org.prebid.server.optout.GoogleRecaptchaVerifier; import org.prebid.server.optout.model.RecaptchaResponse; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; import java.util.Objects; -public class OptoutHandler implements Handler { +public class OptoutHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(OptoutHandler.class); @@ -41,6 +44,13 @@ public OptoutHandler(GoogleRecaptchaVerifier googleRecaptchaVerifier, UidsCookie this.optinUrl = Objects.requireNonNull(optinUrl); } + @Override + public List endpoints() { + return List.of( + HttpEndpoint.of(HttpMethod.GET, Endpoint.optout.value()), + HttpEndpoint.of(HttpMethod.POST, Endpoint.optout.value())); + } + @Override public void handle(RoutingContext routingContext) { final String recaptcha = getRequestParam(routingContext, RECAPTCHA_PARAM); diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 5472e5a36b1..c036bb310cd 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -3,13 +3,11 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; @@ -41,6 +39,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.model.Endpoint; import org.prebid.server.privacy.HostVendorTcfDefinerService; @@ -51,6 +51,8 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.util.Collections; import java.util.List; @@ -63,7 +65,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class SetuidHandler implements Handler { +public class SetuidHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(SetuidHandler.class); @@ -126,6 +128,11 @@ private static Map collectMap(BidderCatalog bidderCa .collect(Collectors.toMap(Usersyncer::getCookieFamilyName, SetuidHandler::preferredUserSyncType)); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value())); + } + private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) { return Stream.of(usersyncer.getIframe(), usersyncer.getRedirect()) .filter(Objects::nonNull) @@ -184,10 +191,7 @@ private Future toSetuidContext(RoutingContext routingContext) { } private Future accountById(String accountId, Timeout timeout) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture(Account.empty(accountId)) - : applicationSettings.getAccountById(accountId, timeout) - .otherwise(Account.empty(accountId)); + return applicationSettings.getAccountById(accountId, timeout).otherwise(Account.empty(accountId)); } private SetuidContext fillWithActivityInfrastructure(SetuidContext setuidContext) { @@ -360,25 +364,31 @@ private void handleErrors(Throwable error, RoutingContext routingContext, TcfCon final String message = error.getMessage(); final HttpResponseStatus status; final String body; - if (error instanceof InvalidRequestException) { - metrics.updateUserSyncBadRequestMetric(); - status = HttpResponseStatus.BAD_REQUEST; - body = "Invalid request format: " + message; - } else if (error instanceof UnauthorizedUidsException) { - metrics.updateUserSyncOptoutMetric(); - status = HttpResponseStatus.UNAUTHORIZED; - body = "Unauthorized: " + message; - } else if (error instanceof UnavailableForLegalReasonsException) { - status = HttpResponseStatus.valueOf(451); - body = "Unavailable For Legal Reasons."; - } else if (error instanceof InvalidAccountConfigException) { - metrics.updateUserSyncBadRequestMetric(); - status = HttpResponseStatus.BAD_REQUEST; - body = "Invalid account configuration: " + message; - } else { - status = HttpResponseStatus.INTERNAL_SERVER_ERROR; - body = "Unexpected setuid processing error: " + message; - logger.warn(body, error); + switch (error) { + case InvalidRequestException invalidRequestException -> { + metrics.updateUserSyncBadRequestMetric(); + status = HttpResponseStatus.BAD_REQUEST; + body = "Invalid request format: " + message; + } + case UnauthorizedUidsException unauthorizedUidsException -> { + metrics.updateUserSyncOptoutMetric(); + status = HttpResponseStatus.UNAUTHORIZED; + body = "Unauthorized: " + message; + } + case UnavailableForLegalReasonsException unavailableForLegalReasonsException -> { + status = HttpResponseStatus.valueOf(451); + body = "Unavailable For Legal Reasons."; + } + case InvalidAccountConfigException invalidAccountConfigException -> { + metrics.updateUserSyncBadRequestMetric(); + status = HttpResponseStatus.BAD_REQUEST; + body = "Invalid account configuration: " + message; + } + default -> { + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + body = "Unexpected setuid processing error: " + message; + logger.warn(body, error); + } } HttpUtil.executeSafely(routingContext, Endpoint.setuid, diff --git a/src/main/java/org/prebid/server/handler/StatusHandler.java b/src/main/java/org/prebid/server/handler/StatusHandler.java index f356971d598..ac4a9983fe7 100644 --- a/src/main/java/org/prebid/server/handler/StatusHandler.java +++ b/src/main/java/org/prebid/server/handler/StatusHandler.java @@ -2,7 +2,7 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.health.HealthChecker; @@ -10,13 +10,16 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.Endpoint; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.TreeMap; import java.util.stream.Collectors; -public class StatusHandler implements Handler { +public class StatusHandler implements ApplicationResource { private final List healthCheckers; private final JacksonMapper mapper; @@ -26,6 +29,11 @@ public StatusHandler(List healthCheckers, JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.status.value())); + } + @Override public void handle(RoutingContext routingContext) { if (CollectionUtils.isEmpty(healthCheckers)) { diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/VtrackHandler.java index d433867b01e..6539881eaa4 100644 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/VtrackHandler.java @@ -5,18 +5,16 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.cache.CacheService; -import org.prebid.server.cache.proto.request.BidCacheRequest; -import org.prebid.server.cache.proto.request.PutObject; -import org.prebid.server.cache.proto.response.BidCacheResponse; +import org.prebid.server.cache.CoreCacheService; +import org.prebid.server.cache.proto.request.bid.BidCacheRequest; +import org.prebid.server.cache.proto.request.bid.BidPutObject; +import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; @@ -24,19 +22,24 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.Endpoint; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -public class VtrackHandler implements Handler { +public class VtrackHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(VtrackHandler.class); @@ -49,7 +52,7 @@ public class VtrackHandler implements Handler { private final boolean modifyVastForUnknownBidder; private final ApplicationSettings applicationSettings; private final BidderCatalog bidderCatalog; - private final CacheService cacheService; + private final CoreCacheService coreCacheService; private final TimeoutFactory timeoutFactory; private final JacksonMapper mapper; @@ -58,7 +61,7 @@ public VtrackHandler(long defaultTimeout, boolean modifyVastForUnknownBidder, ApplicationSettings applicationSettings, BidderCatalog bidderCatalog, - CacheService cacheService, + CoreCacheService coreCacheService, TimeoutFactory timeoutFactory, JacksonMapper mapper) { @@ -67,15 +70,20 @@ public VtrackHandler(long defaultTimeout, this.modifyVastForUnknownBidder = modifyVastForUnknownBidder; this.applicationSettings = Objects.requireNonNull(applicationSettings); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.cacheService = Objects.requireNonNull(cacheService); + this.coreCacheService = Objects.requireNonNull(coreCacheService); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.POST, Endpoint.vtrack.value())); + } + @Override public void handle(RoutingContext routingContext) { final String accountId; - final List vtrackPuts; + final List vtrackPuts; final String integration; try { accountId = accountId(routingContext); @@ -102,7 +110,7 @@ private static String accountId(RoutingContext routingContext) { return accountId; } - private List vtrackPuts(RoutingContext routingContext) { + private List vtrackPuts(RoutingContext routingContext) { final Buffer body = routingContext.getBody(); if (body == null || body.length() == 0) { throw new IllegalArgumentException("Incoming request has no body"); @@ -115,27 +123,27 @@ private List vtrackPuts(RoutingContext routingContext) { throw new IllegalArgumentException("Failed to parse request body", e); } - final List putObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts()); - for (PutObject putObject : putObjects) { - validatePutObject(putObject); + final List bidPutObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts()); + for (BidPutObject bidPutObject : bidPutObjects) { + validatePutObject(bidPutObject); } - return putObjects; + return bidPutObjects; } - private static void validatePutObject(PutObject putObject) { - if (StringUtils.isEmpty(putObject.getBidid())) { + private static void validatePutObject(BidPutObject bidPutObject) { + if (StringUtils.isEmpty(bidPutObject.getBidid())) { throw new IllegalArgumentException("'bidid' is required field and can't be empty"); } - if (StringUtils.isEmpty(putObject.getBidder())) { + if (StringUtils.isEmpty(bidPutObject.getBidder())) { throw new IllegalArgumentException("'bidder' is required field and can't be empty"); } - if (!StringUtils.equals(putObject.getType(), TYPE_XML)) { + if (!StringUtils.equals(bidPutObject.getType(), TYPE_XML)) { throw new IllegalArgumentException("vtrack only accepts type xml"); } - final JsonNode value = putObject.getValue(); + final JsonNode value = bidPutObject.getValue(); final String valueAsString = value != null ? value.asText() : null; if (!StringUtils.containsIgnoreCase(valueAsString, " handleAccountExceptionOrFallback(Throwable except private void handleAccountResult(AsyncResult asyncAccount, RoutingContext routingContext, - List vtrackPuts, + List vtrackPuts, String accountId, String integration, Timeout timeout) { @@ -174,7 +182,8 @@ private void handleAccountResult(AsyncResult asyncAccount, // insert impression tracking if account allows events and bidder allows VAST modification final Boolean isEventEnabled = accountEventsEnabled(asyncAccount.result()); final Set allowedBidders = biddersAllowingVastUpdate(vtrackPuts); - cacheService.cachePutObjects(vtrackPuts, isEventEnabled, allowedBidders, accountId, integration, timeout) + coreCacheService.cachePutObjects( + vtrackPuts, isEventEnabled, allowedBidders, accountId, integration, timeout) .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); } } @@ -190,9 +199,9 @@ private static Boolean accountEventsEnabled(Account account) { /** * Returns list of bidders that allow VAST XML modification. */ - private Set biddersAllowingVastUpdate(List vtrackPuts) { + private Set biddersAllowingVastUpdate(List vtrackPuts) { return vtrackPuts.stream() - .map(PutObject::getBidder) + .map(BidPutObject::getBidder) .filter(this::isAllowVastForBidder) .collect(Collectors.toSet()); } diff --git a/src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java b/src/main/java/org/prebid/server/handler/admin/AccountCacheInvalidationHandler.java similarity index 97% rename from src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java rename to src/main/java/org/prebid/server/handler/admin/AccountCacheInvalidationHandler.java index 8ad70ec3bd0..afe07e9aa67 100644 --- a/src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/AccountCacheInvalidationHandler.java @@ -1,4 +1,4 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; diff --git a/src/main/java/org/prebid/server/handler/admin/AdminResourceWrapper.java b/src/main/java/org/prebid/server/handler/admin/AdminResourceWrapper.java new file mode 100644 index 00000000000..e75920659d6 --- /dev/null +++ b/src/main/java/org/prebid/server/handler/admin/AdminResourceWrapper.java @@ -0,0 +1,45 @@ +package org.prebid.server.handler.admin; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.prebid.server.vertx.verticles.server.admin.AdminResource; + +import java.util.Objects; + +public class AdminResourceWrapper implements AdminResource { + + private final String path; + private final Handler handler; + private final boolean isOnApplicationPort; + private final boolean isSecured; + + public AdminResourceWrapper(String path, + boolean isOnApplicationPort, + boolean isProtected, + Handler handler) { + + this.path = Objects.requireNonNull(path); + this.isOnApplicationPort = isOnApplicationPort; + this.isSecured = isProtected; + this.handler = Objects.requireNonNull(handler); + } + + @Override + public String path() { + return path; + } + + public boolean isOnApplicationPort() { + return isOnApplicationPort; + } + + @Override + public boolean isSecured() { + return isSecured; + } + + @Override + public void handle(RoutingContext routingContext) { + handler.handle(routingContext); + } +} diff --git a/src/main/java/org/prebid/server/handler/CollectedMetricsHandler.java b/src/main/java/org/prebid/server/handler/admin/CollectedMetricsHandler.java similarity index 98% rename from src/main/java/org/prebid/server/handler/CollectedMetricsHandler.java rename to src/main/java/org/prebid/server/handler/admin/CollectedMetricsHandler.java index 0a789d87d92..c9db54e21fa 100644 --- a/src/main/java/org/prebid/server/handler/CollectedMetricsHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/CollectedMetricsHandler.java @@ -1,4 +1,4 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; diff --git a/src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java b/src/main/java/org/prebid/server/handler/admin/CurrencyRatesHandler.java similarity index 96% rename from src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java rename to src/main/java/org/prebid/server/handler/admin/CurrencyRatesHandler.java index fa7d54cafd4..c57e61b9b71 100644 --- a/src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/CurrencyRatesHandler.java @@ -1,16 +1,16 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import com.fasterxml.jackson.annotation.JsonProperty; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; import java.io.IOException; diff --git a/src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java b/src/main/java/org/prebid/server/handler/admin/HttpInteractionLogHandler.java similarity index 99% rename from src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java rename to src/main/java/org/prebid/server/handler/admin/HttpInteractionLogHandler.java index a6c17f0c4cd..b3573a5c4c5 100644 --- a/src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/HttpInteractionLogHandler.java @@ -1,4 +1,4 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; diff --git a/src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java b/src/main/java/org/prebid/server/handler/admin/LoggerControlKnobHandler.java similarity index 98% rename from src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java rename to src/main/java/org/prebid/server/handler/admin/LoggerControlKnobHandler.java index 61b6e305718..7875c7a52dd 100644 --- a/src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/LoggerControlKnobHandler.java @@ -1,4 +1,4 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; diff --git a/src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java b/src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java similarity index 90% rename from src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java rename to src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java index 08aec26922b..d237a542ed7 100644 --- a/src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/SettingsCacheNotificationHandler.java @@ -1,8 +1,9 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -31,10 +32,13 @@ public SettingsCacheNotificationHandler(CacheNotificationListener cacheNotificat @Override public void handle(RoutingContext routingContext) { - switch (routingContext.request().method()) { - case POST -> doSave(routingContext); - case DELETE -> doInvalidate(routingContext); - default -> doFail(routingContext); + final HttpMethod method = routingContext.request().method(); + if (method.equals(HttpMethod.POST)) { + doSave(routingContext); + } else if (method.equals(HttpMethod.DELETE)) { + doInvalidate(routingContext); + } else { + doFail(routingContext); } } diff --git a/src/main/java/org/prebid/server/handler/TracerLogHandler.java b/src/main/java/org/prebid/server/handler/admin/TracerLogHandler.java similarity index 77% rename from src/main/java/org/prebid/server/handler/TracerLogHandler.java rename to src/main/java/org/prebid/server/handler/admin/TracerLogHandler.java index ae0412860ae..d2414012b60 100644 --- a/src/main/java/org/prebid/server/handler/TracerLogHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/TracerLogHandler.java @@ -1,4 +1,4 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; @@ -13,7 +13,6 @@ public class TracerLogHandler implements Handler { private static final String ACCOUNT_PARAMETER = "account"; - private static final String LINE_ITEM_PARAMETER = "lineItemId"; private static final String BIDDER_CODE_PARAMETER = "bidderCode"; private static final String LOG_LEVEL_PARAMETER = "level"; private static final String DURATION_IN_SECONDS = "duration"; @@ -29,11 +28,11 @@ public void handle(RoutingContext routingContext) { final MultiMap parameters = routingContext.request().params(); final String accountId = parameters.get(ACCOUNT_PARAMETER); final String bidderCode = parameters.get(BIDDER_CODE_PARAMETER); - final String lineItemId = parameters.get(LINE_ITEM_PARAMETER); - if (StringUtils.isBlank(accountId) && StringUtils.isBlank(lineItemId) && StringUtils.isBlank(bidderCode)) { - routingContext.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) - .end("At least one parameter should ne defined: account, bidderCode, lineItemId"); + if (StringUtils.isBlank(accountId) && StringUtils.isBlank(bidderCode)) { + routingContext.response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end("At least one parameter should be defined: account, bidderCode"); return; } @@ -42,14 +41,17 @@ public void handle(RoutingContext routingContext) { try { duration = parseDuration(parameters.get(DURATION_IN_SECONDS)); } catch (InvalidRequestException e) { - routingContext.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(e.getMessage()); + routingContext.response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(e.getMessage()); return; } try { - criteriaManager.addCriteria(accountId, bidderCode, lineItemId, loggerLevel, duration); + criteriaManager.addCriteria(accountId, bidderCode, loggerLevel, duration); } catch (IllegalArgumentException e) { - routingContext.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + routingContext.response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) .end("Invalid parameter: " + e.getMessage()); return; } @@ -67,6 +69,5 @@ private static int parseDuration(String rawDuration) { throw new InvalidRequestException( "duration parameter should be defined as integer, but was " + rawDuration); } - } } diff --git a/src/main/java/org/prebid/server/handler/VersionHandler.java b/src/main/java/org/prebid/server/handler/admin/VersionHandler.java similarity index 94% rename from src/main/java/org/prebid/server/handler/VersionHandler.java rename to src/main/java/org/prebid/server/handler/admin/VersionHandler.java index eeeecae349a..1c48f79a74a 100644 --- a/src/main/java/org/prebid/server/handler/VersionHandler.java +++ b/src/main/java/org/prebid/server/handler/admin/VersionHandler.java @@ -1,15 +1,15 @@ -package org.prebid.server.handler; +package org.prebid.server.handler.admin; import com.fasterxml.jackson.core.JsonProcessingException; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; import lombok.Value; import org.apache.commons.lang3.StringUtils; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; import java.util.Objects; diff --git a/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java b/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java index 4a35cd8a9fc..007dd187db3 100644 --- a/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java +++ b/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import lombok.Value; import org.apache.commons.collections4.map.CaseInsensitiveMap; @@ -13,8 +13,11 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.Endpoint; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; @@ -22,7 +25,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class BidderDetailsHandler implements Handler { +public class BidderDetailsHandler implements ApplicationResource { private static final String BIDDER_NAME_PARAM = "bidderName"; private static final String ALL_PARAM_VALUE = "all"; @@ -67,6 +70,12 @@ private ObjectNode allInfos(Map nameToInfo) { return mapper.mapper().valueToTree(new TreeMap<>(nameToInfo)); } + @Override + public List endpoints() { + return Collections.singletonList( + HttpEndpoint.of(HttpMethod.GET, "%s/:%s".formatted(Endpoint.info_bidders.value(), BIDDER_NAME_PARAM))); + } + @Override public void handle(RoutingContext routingContext) { final String bidderName = routingContext.request().getParam(BIDDER_NAME_PARAM); diff --git a/src/main/java/org/prebid/server/handler/info/BiddersHandler.java b/src/main/java/org/prebid/server/handler/info/BiddersHandler.java index a212f2b8c04..ff186e5d63c 100644 --- a/src/main/java/org/prebid/server/handler/info/BiddersHandler.java +++ b/src/main/java/org/prebid/server/handler/info/BiddersHandler.java @@ -2,14 +2,17 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.handler.info.filters.BidderInfoFilterStrategy; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.Endpoint; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @@ -17,7 +20,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -public class BiddersHandler implements Handler { +public class BiddersHandler implements ApplicationResource { private final BidderCatalog bidderCatalog; private final List filterStrategies; @@ -32,6 +35,11 @@ public BiddersHandler(BidderCatalog bidderCatalog, this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.info_bidders.value())); + } + @Override public void handle(RoutingContext routingContext) { try { diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java index d2f94d27e67..c1d9c58dca6 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java @@ -12,11 +12,9 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; @@ -30,8 +28,8 @@ import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.exception.BlacklistedAccountException; -import org.prebid.server.exception.BlacklistedAppException; +import org.prebid.server.exception.BlocklistedAccountException; +import org.prebid.server.exception.BlocklistedAppException; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; @@ -39,6 +37,8 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.Endpoint; @@ -56,6 +56,8 @@ import org.prebid.server.proto.response.ExtAmpVideoResponse; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.time.Clock; import java.util.Collections; @@ -67,7 +69,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -public class AmpHandler implements Handler { +public class AmpHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(AmpHandler.class); private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); @@ -115,6 +117,11 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, this.logSamplingRate = logSamplingRate; } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.openrtb2_amp.value())); + } + @Override public void handle(RoutingContext routingContext) { // Prebid Server interprets request.tmax to be the maximum amount of time that a caller is willing to wait @@ -315,11 +322,12 @@ private void handleResult(AsyncResult> respo status = HttpResponseStatus.UNAUTHORIZED; body = message; - } else if (exception instanceof BlacklistedAppException - || exception instanceof BlacklistedAccountException) { - metricRequestStatus = exception instanceof BlacklistedAccountException - ? MetricName.blacklisted_account : MetricName.blacklisted_app; - final String message = "Blacklisted: " + exception.getMessage(); + } else if (exception instanceof BlocklistedAppException + || exception instanceof BlocklistedAccountException) { + metricRequestStatus = exception instanceof BlocklistedAccountException + ? MetricName.blocklisted_account + : MetricName.blocklisted_app; + final String message = "Blocklisted: " + exception.getMessage(); logger.debug(message); errorMessages = Collections.singletonList(message); @@ -361,7 +369,7 @@ private static String originFrom(RoutingContext routingContext) { String origin = null; final List ampSourceOrigin = routingContext.queryParam("__amp_source_origin"); if (CollectionUtils.isNotEmpty(ampSourceOrigin)) { - origin = ampSourceOrigin.get(0); + origin = ampSourceOrigin.getFirst(); } if (origin == null) { // Just to be safe @@ -370,8 +378,13 @@ private static String originFrom(RoutingContext routingContext) { return origin; } - private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body, long startTime, - MetricName metricRequestStatus, AmpEvent event, TcfContext tcfContext) { + private void respondWith(RoutingContext routingContext, + HttpResponseStatus status, + String body, + long startTime, + MetricName metricRequestStatus, + AmpEvent event, + TcfContext tcfContext) { final boolean responseSent = HttpUtil.executeSafely(routingContext, Endpoint.openrtb2_amp, response -> response @@ -389,7 +402,7 @@ private void respondWith(RoutingContext routingContext, HttpResponseStatus statu } private void handleResponseException(Throwable exception) { - logger.warn("Failed to send amp response: {0}", exception.getMessage()); + logger.warn("Failed to send amp response: {}", exception.getMessage()); metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java index 043890ae86b..b8664bc75fd 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java @@ -5,26 +5,28 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; -import io.vertx.core.Handler; +import io.vertx.core.Future; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.exception.BlacklistedAccountException; -import org.prebid.server.exception.BlacklistedAppException; +import org.prebid.server.exception.BlocklistedAccountException; +import org.prebid.server.exception.BlocklistedAppException; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.Endpoint; @@ -33,6 +35,8 @@ import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.time.Clock; import java.util.Collections; @@ -40,7 +44,7 @@ import java.util.Objects; import java.util.function.Consumer; -public class AuctionHandler implements Handler { +public class AuctionHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(AuctionHandler.class); private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); @@ -48,6 +52,7 @@ public class AuctionHandler implements Handler { private final double logSamplingRate; private final AuctionRequestFactory auctionRequestFactory; private final ExchangeService exchangeService; + private final SkippedAuctionService skippedAuctionService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final Clock clock; @@ -58,6 +63,7 @@ public class AuctionHandler implements Handler { public AuctionHandler(double logSamplingRate, AuctionRequestFactory auctionRequestFactory, ExchangeService exchangeService, + SkippedAuctionService skippedAuctionService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, Clock clock, @@ -68,6 +74,7 @@ public AuctionHandler(double logSamplingRate, this.logSamplingRate = logSamplingRate; this.auctionRequestFactory = Objects.requireNonNull(auctionRequestFactory); this.exchangeService = Objects.requireNonNull(exchangeService); + this.skippedAuctionService = Objects.requireNonNull(skippedAuctionService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); @@ -76,6 +83,11 @@ public AuctionHandler(double logSamplingRate, this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.POST, Endpoint.openrtb2_auction.value())); + } + @Override public void handle(RoutingContext routingContext) { // Prebid Server interprets request.tmax to be the maximum amount of time that a caller is willing to wait @@ -87,8 +99,16 @@ public void handle(RoutingContext routingContext) { final AuctionEvent.AuctionEventBuilder auctionEventBuilder = AuctionEvent.builder() .httpContext(HttpRequestContext.from(routingContext)); - auctionRequestFactory.fromRequest(routingContext, startTime) + auctionRequestFactory.parseRequest(routingContext, startTime) + .compose(auctionContext -> skippedAuctionService.skipAuction(auctionContext) + .recover(throwable -> holdAuction(auctionEventBuilder, auctionContext))) + .onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime)); + } + + private Future holdAuction(AuctionEvent.AuctionEventBuilder auctionEventBuilder, + AuctionContext auctionContext) { + return auctionRequestFactory.enrichAuctionContext(auctionContext) .map(this::updateAppAndNoCookieAndImpsMetrics) // In case of holdAuction Exception and auctionContext is not present below @@ -97,8 +117,7 @@ public void handle(RoutingContext routingContext) { .compose(exchangeService::holdAuction) // populate event with updated context .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context)) - .onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime)); + .map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context)); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -127,9 +146,11 @@ private void handleResult(AsyncResult responseResult, AuctionEvent.AuctionEventBuilder auctionEventBuilder, RoutingContext routingContext, long startTime) { + final boolean responseSucceeded = responseResult.succeeded(); final AuctionContext auctionContext = responseSucceeded ? responseResult.result() : null; + final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped(); final MetricName requestType = responseSucceeded ? auctionContext.getRequestTypeMetric() : MetricName.openrtb2web; @@ -172,11 +193,12 @@ private void handleResult(AsyncResult responseResult, status = HttpResponseStatus.UNAUTHORIZED; body = message; - } else if (exception instanceof BlacklistedAppException - || exception instanceof BlacklistedAccountException) { - metricRequestStatus = exception instanceof BlacklistedAccountException - ? MetricName.blacklisted_account : MetricName.blacklisted_app; - final String message = "Blacklisted: " + exception.getMessage(); + } else if (exception instanceof BlocklistedAppException + || exception instanceof BlocklistedAccountException) { + metricRequestStatus = exception instanceof BlocklistedAccountException + ? MetricName.blocklisted_account + : MetricName.blocklisted_app; + final String message = "Blocklisted: " + exception.getMessage(); logger.debug(message); errorMessages = Collections.singletonList(message); @@ -205,33 +227,39 @@ private void handleResult(AsyncResult responseResult, final AuctionEvent auctionEvent = auctionEventBuilder.status(status.code()).errors(errorMessages).build(); final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); - respondWith(routingContext, status, body, startTime, requestType, metricRequestStatus, auctionEvent, - tcfContext); + + final boolean responseSent = respondWith(routingContext, status, body, requestType); + + if (responseSent) { + metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime); + metrics.updateRequestTypeMetric(requestType, metricRequestStatus); + if (!isAuctionSkipped) { + analyticsDelegator.processEvent(auctionEvent, tcfContext); + } + } else { + metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); + } httpInteractionLogger.maybeLogOpenrtb2Auction(auctionContext, routingContext, status.code(), body); } - private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body, long startTime, - MetricName requestType, MetricName metricRequestStatus, AuctionEvent event, - TcfContext tcfContext) { + private boolean respondWith(RoutingContext routingContext, + HttpResponseStatus status, + String body, + MetricName requestType) { - final boolean responseSent = HttpUtil.executeSafely(routingContext, Endpoint.openrtb2_auction, + return HttpUtil.executeSafely( + routingContext, + Endpoint.openrtb2_auction, response -> response .exceptionHandler(throwable -> handleResponseException(throwable, requestType)) .setStatusCode(status.code()) .end(body)); - if (responseSent) { - metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime); - metrics.updateRequestTypeMetric(requestType, metricRequestStatus); - analyticsDelegator.processEvent(event, tcfContext); - } else { - metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); - } } private void handleResponseException(Throwable throwable, MetricName requestType) { - logger.warn("Failed to send auction response: {0}", throwable.getMessage()); + logger.warn("Failed to send auction response: {}", throwable.getMessage()); metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java index 144338430ba..d5957c15aa7 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -3,11 +3,9 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; -import io.vertx.core.Handler; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; @@ -17,10 +15,12 @@ import org.prebid.server.auction.model.CachedDebugLog; import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.auction.requestfactory.VideoRequestFactory; -import org.prebid.server.cache.CacheService; +import org.prebid.server.cache.CoreCacheService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.Endpoint; @@ -33,6 +33,8 @@ import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.time.Clock; import java.util.ArrayList; @@ -42,7 +44,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -public class VideoHandler implements Handler { +public class VideoHandler implements ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(VideoHandler.class); @@ -51,7 +53,7 @@ public class VideoHandler implements Handler { private final VideoRequestFactory videoRequestFactory; private final VideoResponseFactory videoResponseFactory; private final ExchangeService exchangeService; - private final CacheService cacheService; + private final CoreCacheService coreCacheService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final Clock clock; @@ -61,15 +63,16 @@ public class VideoHandler implements Handler { public VideoHandler(VideoRequestFactory videoRequestFactory, VideoResponseFactory videoResponseFactory, ExchangeService exchangeService, - CacheService cacheService, AnalyticsReporterDelegator analyticsDelegator, + CoreCacheService coreCacheService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, Clock clock, PrebidVersionProvider prebidVersionProvider, JacksonMapper mapper) { + this.videoRequestFactory = Objects.requireNonNull(videoRequestFactory); this.videoResponseFactory = Objects.requireNonNull(videoResponseFactory); this.exchangeService = Objects.requireNonNull(exchangeService); - this.cacheService = Objects.requireNonNull(cacheService); + this.coreCacheService = Objects.requireNonNull(coreCacheService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); @@ -77,6 +80,11 @@ public VideoHandler(VideoRequestFactory videoRequestFactory, this.mapper = Objects.requireNonNull(mapper); } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.POST, Endpoint.openrtb2_video.value())); + } + @Override public void handle(RoutingContext routingContext) { // Prebid Server interprets request.tmax to be the maximum amount of time that a caller is willing to wait @@ -139,7 +147,7 @@ private void handleResult(AsyncResult responseResult, if (exception instanceof InvalidRequestException) { metricRequestStatus = MetricName.badinput; errorMessages = ((InvalidRequestException) exception).getMessages(); - logger.info("Invalid request format: {0}", errorMessages); + logger.info("Invalid request format: {}", errorMessages); status = HttpResponseStatus.BAD_REQUEST; body = errorMessages.stream() @@ -148,7 +156,7 @@ private void handleResult(AsyncResult responseResult, } else if (exception instanceof UnauthorizedAccountException) { metricRequestStatus = MetricName.badinput; final String errorMessage = exception.getMessage(); - logger.info("Unauthorized: {0}", errorMessage); + logger.info("Unauthorized: {}", errorMessage); errorMessages = Collections.singletonList(errorMessage); status = HttpResponseStatus.UNAUTHORIZED; @@ -194,7 +202,7 @@ private String cacheDebugLog(AuctionContext auctionContext, List errors) final Integer videoCacheTtl = ObjectUtil.getIfNotNull(accountAuctionConfig, AccountAuctionConfig::getVideoCacheTtl); - return cacheService.cacheVideoDebugLog(cachedDebugLog, videoCacheTtl); + return coreCacheService.cacheVideoDebugLog(cachedDebugLog, videoCacheTtl); } private VideoEvent updateEventWithDebugCacheMessage(VideoEvent videoEvent, String cacheKey) { @@ -228,7 +236,7 @@ private void respondWith(RoutingContext routingContext, } private void handleResponseException(Throwable throwable) { - logger.warn("Failed to send video response: {0}", throwable.getMessage()); + logger.warn("Failed to send video response: {}", throwable.getMessage()); metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } diff --git a/src/main/java/org/prebid/server/health/DatabaseHealthChecker.java b/src/main/java/org/prebid/server/health/DatabaseHealthChecker.java index e27a72dd0db..6949aba2d12 100644 --- a/src/main/java/org/prebid/server/health/DatabaseHealthChecker.java +++ b/src/main/java/org/prebid/server/health/DatabaseHealthChecker.java @@ -1,9 +1,7 @@ package org.prebid.server.health; -import io.vertx.core.Promise; import io.vertx.core.Vertx; -import io.vertx.ext.jdbc.JDBCClient; -import io.vertx.ext.sql.SQLConnection; +import io.vertx.sqlclient.Pool; import org.prebid.server.health.model.Status; import org.prebid.server.health.model.StatusResponse; @@ -15,13 +13,13 @@ public class DatabaseHealthChecker extends PeriodicHealthChecker { private static final String NAME = "database"; - private final JDBCClient jdbcClient; + private final Pool pool; private StatusResponse status; - public DatabaseHealthChecker(Vertx vertx, JDBCClient jdbcClient, long refreshPeriod) { + public DatabaseHealthChecker(Vertx vertx, Pool pool, long refreshPeriod) { super(vertx, refreshPeriod); - this.jdbcClient = Objects.requireNonNull(jdbcClient); + this.pool = Objects.requireNonNull(pool); } @Override @@ -36,9 +34,7 @@ public String name() { @Override void updateStatus() { - final Promise connectionPromise = Promise.promise(); - jdbcClient.getConnection(connectionPromise); - connectionPromise.future().onComplete(result -> + pool.getConnection().onComplete(result -> status = StatusResponse.of( result.succeeded() ? Status.UP.name() : Status.DOWN.name(), ZonedDateTime.now(Clock.systemUTC()))); diff --git a/src/main/java/org/prebid/server/health/HealthMonitor.java b/src/main/java/org/prebid/server/health/HealthMonitor.java deleted file mode 100644 index 3fc8a53ed2a..00000000000 --- a/src/main/java/org/prebid/server/health/HealthMonitor.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.prebid.server.health; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.concurrent.atomic.LongAdder; - -/** - * Used to gather statistics and calculate the health index indicator. - */ -public class HealthMonitor { - - private final LongAdder totalCounter = new LongAdder(); - - private final LongAdder successCounter = new LongAdder(); - - /** - * Increments total number of requests. - */ - public void incTotal() { - totalCounter.increment(); - } - - /** - * Increments succeeded number of requests. - */ - public void incSuccess() { - successCounter.increment(); - } - - /** - * Returns value between 0.0 ... 1.0 where 1.0 is indicated 100% healthy. - */ - public BigDecimal calculateHealthIndex() { - final BigDecimal success = BigDecimal.valueOf(successCounter.sumThenReset()); - final BigDecimal total = BigDecimal.valueOf(totalCounter.sumThenReset()); - return total.longValue() == 0 ? BigDecimal.ONE : success.divide(total, 2, RoundingMode.HALF_EVEN); - } -} diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java index 394477d1be4..2525651f872 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -4,7 +4,6 @@ import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookId; @@ -12,6 +11,7 @@ import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.time.Clock; import java.util.concurrent.TimeoutException; diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java index 47ee0cc3019..a4487e3a60b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.execution; -import io.vertx.core.logging.LoggerFactory; import lombok.Getter; import lombok.experimental.Accessors; import org.prebid.server.hooks.execution.model.ExecutionAction; @@ -13,6 +12,7 @@ import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java index c268731a157..c5c798f125f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.execution.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @@ -11,5 +12,6 @@ public class ExecutionGroup { Long timeout; @JsonProperty("hook-sequence") + @JsonAlias("hook_sequence") List hookSequence; } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/HookId.java b/src/main/java/org/prebid/server/hooks/execution/model/HookId.java index 8d7db1da069..31e143ee15b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/HookId.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/HookId.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.execution.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @@ -7,8 +8,10 @@ public class HookId { @JsonProperty("module-code") + @JsonAlias("module_code") String moduleCode; @JsonProperty("hook-impl-code") + @JsonAlias("hook_impl_code") String hookImplCode; } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java index 6ee9908a7e2..47896d8c9ab 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java @@ -1,38 +1,37 @@ package org.prebid.server.hooks.execution.model; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; public enum Stage { entrypoint, - raw_auction_request("raw-auction-request"), - processed_auction_request("processed-auction-request"), - bidder_request("bidder-request"), - raw_bidder_response("raw-bidder-response"), - processed_bidder_response("processed-bidder-response"), - all_processed_bid_responses("all-processed-bid-responses"), - auction_response("auction-response"); - - @JsonValue - private final String value; - - Stage() { - this.value = name(); - } - - Stage(String value) { - this.value = value; - } - - @SuppressWarnings("unused") - @JsonCreator - public static Stage fromString(String value) { - return Arrays.stream(values()) - .filter(stage -> stage.value.equals(value)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Unknown stage")); - } + + @JsonProperty("raw-auction-request") + @JsonAlias("raw_auction_request") + raw_auction_request, + + @JsonProperty("processed-auction-request") + @JsonAlias("processed_auction_request") + processed_auction_request, + + @JsonProperty("bidder-request") + @JsonAlias("bidder_request") + bidder_request, + + @JsonProperty("raw-bidder-response") + @JsonAlias("raw_bidder_response") + raw_bidder_response, + + @JsonProperty("processed-bidder-response") + @JsonAlias("processed_bidder_response") + processed_bidder_response, + + @JsonProperty("all-processed-bid-responses") + @JsonAlias("all_processed_bid_responses") + all_processed_bid_responses, + + @JsonProperty("auction-response") + @JsonAlias("auction_response") + auction_response } diff --git a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java index 96189d6706f..fc1bcea0611 100644 --- a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java +++ b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.StreamReadFeature; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -17,7 +18,9 @@ public final class ObjectMapperProvider { MAPPER = JsonMapper.builder().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) - .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN).build() + .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) + .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) + .build() .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .registerModule(new BlackbirdModule()) diff --git a/src/main/java/org/prebid/server/log/ConditionalLogger.java b/src/main/java/org/prebid/server/log/ConditionalLogger.java index 899c0d25251..f3e17bb9ed0 100644 --- a/src/main/java/org/prebid/server/log/ConditionalLogger.java +++ b/src/main/java/org/prebid/server/log/ConditionalLogger.java @@ -1,7 +1,6 @@ package org.prebid.server.log; import com.github.benmanes.caffeine.cache.Caffeine; -import io.vertx.core.logging.Logger; import org.apache.commons.lang3.ObjectUtils; import java.time.Instant; diff --git a/src/main/java/org/prebid/server/log/Criteria.java b/src/main/java/org/prebid/server/log/Criteria.java index f98a45c2500..dd6f3cd123f 100644 --- a/src/main/java/org/prebid/server/log/Criteria.java +++ b/src/main/java/org/prebid/server/log/Criteria.java @@ -1,48 +1,31 @@ package org.prebid.server.log; -import io.vertx.core.logging.Logger; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; +import org.apache.commons.lang3.StringUtils; import java.util.Objects; import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; -@Value -@Builder -@AllArgsConstructor public class Criteria { private static final String TAG_SEPARATOR = "-"; - private static final String TAGGED_MESSAGE_PATTERN = "[%s]: %s"; private static final String TAGGED_RESPONSE_PATTERN = "[%s]: %s - %s"; public static final String BID_RESPONSE = "BidResponse"; public static final String RESOLVED_BID_REQUEST = "Resolved BidRequest"; - String account; + private final String account; + private final String bidder; + private final String tag; + private final BiConsumer loggerLevel; - String bidder; - - String lineItemId; - - String tag; - - BiConsumer loggerLevel; - - public static Criteria create(String account, String bidder, String lineItemId, - BiConsumer loggerLevel) { - return new Criteria(account, bidder, lineItemId, makeTag(account, bidder, lineItemId), loggerLevel); + private Criteria(String account, String bidder, BiConsumer loggerLevel) { + this.account = account; + this.bidder = bidder; + this.tag = makeTag(account, bidder); + this.loggerLevel = Objects.requireNonNull(loggerLevel); } - public void log(Criteria criteria, Logger logger, Object message, Consumer defaultLogger) { - if (isMatched(criteria)) { - loggerLevel.accept(logger, TAGGED_MESSAGE_PATTERN.formatted(tag, message)); - } else { - defaultLogger.accept(message); - } + public static Criteria create(String account, String bidder, BiConsumer loggerLevel) { + return new Criteria(account, bidder, loggerLevel); } public void logResponse(String bidResponse, Logger logger) { @@ -58,23 +41,19 @@ public void logResponseAndRequest(String bidResponse, String bidRequest, Logger } } - private boolean isMatched(Criteria criteria) { - return criteria != null - && (account == null || account.equals(criteria.account)) - && (bidder == null || bidder.equals(criteria.bidder)) - && (lineItemId == null || lineItemId.equals(criteria.lineItemId)); - } - private boolean isMatchedToString(String value) { return (account == null || value.contains(account)) - && (bidder == null || value.contains(bidder)) - && (lineItemId == null || value.contains(lineItemId)); + && (bidder == null || value.contains(bidder)); } - private static String makeTag(String account, String bidder, String lineItemId) { - return Stream.of(account, bidder, lineItemId) - .filter(Objects::nonNull) - .collect(Collectors.joining(TAG_SEPARATOR)); - } + private static String makeTag(String account, String bidder) { + if (account == null) { + return StringUtils.defaultString(bidder); + } + if (bidder == null) { + return account; + } + return account + TAG_SEPARATOR + bidder; + } } diff --git a/src/main/java/org/prebid/server/log/CriteriaLogManager.java b/src/main/java/org/prebid/server/log/CriteriaLogManager.java index b8e5801d436..75ca28f5f81 100644 --- a/src/main/java/org/prebid/server/log/CriteriaLogManager.java +++ b/src/main/java/org/prebid/server/log/CriteriaLogManager.java @@ -3,14 +3,11 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import io.vertx.core.impl.ConcurrentHashSet; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; public class CriteriaLogManager { @@ -24,25 +21,11 @@ public CriteriaLogManager(JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper); } - public void log(Logger logger, Criteria criteria, Object message, Consumer defaultLogger) { - if (criterias.isEmpty()) { - defaultLogger.accept(message); - } - criterias.forEach(cr -> cr.log(criteria, logger, message, defaultLogger)); - } - - public void log(Logger logger, String account, Object message, Consumer defaultLogger) { - log(logger, Criteria.builder().account(account).build(), message, defaultLogger); - } - - public void log(Logger logger, String account, String bidder, String lineItemId, Object message, - Consumer defaultLogger) { - log(logger, Criteria.builder().account(account).bidder(bidder).lineItemId(lineItemId).build(), - message, defaultLogger); - } - - public BidResponse traceResponse(Logger logger, BidResponse bidResponse, BidRequest bidRequest, + public BidResponse traceResponse(Logger logger, + BidResponse bidResponse, + BidRequest bidRequest, boolean debugEnabled) { + if (criterias.isEmpty()) { return bidResponse; } @@ -53,7 +36,7 @@ public BidResponse traceResponse(Logger logger, BidResponse bidResponse, BidRequ jsonBidResponse = mapper.encodeToString(bidResponse); jsonBidRequest = debugEnabled ? null : mapper.encodeToString(bidRequest); } catch (EncodeException e) { - CriteriaLogManager.logger.warn("Failed to parse bidResponse or bidRequest to json string: {0}", e); + CriteriaLogManager.logger.warn("Failed to parse bidResponse or bidRequest to json string: {}", e); return bidResponse; } diff --git a/src/main/java/org/prebid/server/log/CriteriaManager.java b/src/main/java/org/prebid/server/log/CriteriaManager.java index e5cde95a392..8602f182d2e 100644 --- a/src/main/java/org/prebid/server/log/CriteriaManager.java +++ b/src/main/java/org/prebid/server/log/CriteriaManager.java @@ -1,19 +1,13 @@ package org.prebid.server.log; import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.deals.model.LogCriteriaFilter; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; public class CriteriaManager { private static final long MAX_CRITERIA_DURATION = 300000L; - private static final Logger logger = LoggerFactory.getLogger(CriteriaManager.class); - private final CriteriaLogManager criteriaLogManager; private final Vertx vertx; @@ -22,24 +16,16 @@ public CriteriaManager(CriteriaLogManager criteriaLogManager, Vertx vertx) { this.vertx = vertx; } - public void addCriteria(String accountId, String bidderCode, String lineItemId, String loggerLevel, + public void addCriteria(String accountId, + String bidderCode, + String loggerLevel, Integer durationMillis) { - final Criteria criteria = Criteria.create(accountId, bidderCode, lineItemId, resolveLogLevel(loggerLevel)); + + final Criteria criteria = Criteria.create(accountId, bidderCode, resolveLogLevel(loggerLevel)); criteriaLogManager.addCriteria(criteria); vertx.setTimer(limitDuration(durationMillis), ignored -> criteriaLogManager.removeCriteria(criteria)); } - public void addCriteria(LogCriteriaFilter filter, Long durationSeconds) { - if (filter != null) { - final Criteria criteria = Criteria.create(filter.getAccountId(), filter.getBidderCode(), - filter.getLineItemId(), Logger::error); - criteriaLogManager.addCriteria(criteria); - logger.info("Logger was updated with new criteria {0}", criteria); - vertx.setTimer(limitDuration(TimeUnit.SECONDS.toMillis(durationSeconds)), - ignored -> criteriaLogManager.removeCriteria(criteria)); - } - } - public void stop() { criteriaLogManager.removeAllCriteria(); } @@ -49,21 +35,18 @@ private long limitDuration(long durationMillis) { } private BiConsumer resolveLogLevel(String rawLogLevel) { - final LogLevel logLevel; try { - logLevel = LogLevel.valueOf(rawLogLevel.toLowerCase()); + return switch (LogLevel.valueOf(rawLogLevel.toLowerCase())) { + case info -> Logger::info; + case warn -> Logger::warn; + case trace -> Logger::trace; + case error -> Logger::error; + case fatal -> Logger::fatal; + case debug -> Logger::debug; + }; } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid LoggingLevel: " + rawLogLevel); } - - return switch (logLevel) { - case info -> Logger::info; - case warn -> Logger::warn; - case trace -> Logger::trace; - case error -> Logger::error; - case fatal -> Logger::fatal; - case debug -> Logger::debug; - }; } private enum LogLevel { diff --git a/src/main/java/org/prebid/server/log/HttpInteractionLogger.java b/src/main/java/org/prebid/server/log/HttpInteractionLogger.java index 15cf54303c0..cc89b9cb57b 100644 --- a/src/main/java/org/prebid/server/log/HttpInteractionLogger.java +++ b/src/main/java/org/prebid/server/log/HttpInteractionLogger.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import lombok.Value; import org.apache.commons.collections4.CollectionUtils; @@ -47,7 +45,7 @@ public void maybeLogOpenrtb2Auction(AuctionContext auctionContext, if (interactionSatisfiesSpec(HttpLogSpec.Endpoint.auction, statusCode, auctionContext)) { logger.info( - "Requested URL: \"{0}\", request body: \"{1}\", response status: \"{2}\", response body: \"{3}\"", + "Requested URL: \"{}\", request body: \"{}\", response status: \"{}\", response body: \"{}\"", routingContext.request().uri(), toOneLineString(routingContext.getBodyAsString()), statusCode, @@ -72,7 +70,7 @@ public void maybeLogOpenrtb2Amp(AuctionContext auctionContext, if (interactionSatisfiesSpec(HttpLogSpec.Endpoint.amp, statusCode, auctionContext)) { logger.info( - "Requested URL: \"{0}\", response status: \"{1}\", response body: \"{2}\"", + "Requested URL: \"{}\", response status: \"{}\", response body: \"{}\"", routingContext.request().uri(), statusCode, responseBody); @@ -87,7 +85,7 @@ public void maybeLogBidderRequest(AuctionContext context, BidderRequest bidderRe final BidRequest bidRequest = bidderRequest.getBidRequest(); final BidRequest updatedBidRequest = bidRequestWithBidderName(bidder, bidRequest); final String jsonBidRequest = mapper.encodeToString(updatedBidRequest); - logger.info("Request body to {0}: \"{1}\"", bidder, jsonBidRequest); + logger.info("Request body to {}: \"{}\"", bidder, jsonBidRequest); incLoggedInteractions(); } diff --git a/src/main/java/org/prebid/server/log/Logger.java b/src/main/java/org/prebid/server/log/Logger.java new file mode 100644 index 00000000000..9a595f0de2e --- /dev/null +++ b/src/main/java/org/prebid/server/log/Logger.java @@ -0,0 +1,141 @@ +package org.prebid.server.log; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.message.FormattedMessage; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.spi.ExtendedLogger; + +public class Logger { + + private static final String FQCN = Logger.class.getCanonicalName(); + + private final ExtendedLogger delegate; + + Logger(ExtendedLogger delegate) { + this.delegate = delegate; + } + + public boolean isWarnEnabled() { + return delegate.isWarnEnabled(); + } + + public boolean isInfoEnabled() { + return delegate.isInfoEnabled(); + } + + public boolean isDebugEnabled() { + return delegate.isDebugEnabled(); + } + + public boolean isTraceEnabled() { + return delegate.isTraceEnabled(); + } + + public void fatal(Object message) { + log(Level.FATAL, message); + } + + public void fatal(Object message, Throwable t) { + log(Level.FATAL, message, t); + } + + public void error(Object message) { + log(Level.ERROR, message); + } + + public void error(Object message, Object... params) { + log(Level.ERROR, message.toString(), params); + } + + public void error(Object message, Throwable t) { + log(Level.ERROR, message, t); + } + + public void error(Object message, Throwable t, Object... params) { + log(Level.ERROR, message.toString(), t, params); + } + + public void warn(Object message) { + log(Level.WARN, message); + } + + public void warn(Object message, Object... params) { + log(Level.WARN, message.toString(), params); + } + + public void warn(Object message, Throwable t) { + log(Level.WARN, message, t); + } + + public void warn(Object message, Throwable t, Object... params) { + log(Level.WARN, message.toString(), t, params); + } + + public void info(Object message) { + log(Level.INFO, message); + } + + public void info(Object message, Object... params) { + log(Level.INFO, message.toString(), params); + } + + public void info(Object message, Throwable t) { + log(Level.INFO, message, t); + } + + public void info(Object message, Throwable t, Object... params) { + log(Level.INFO, message.toString(), t, params); + } + + public void debug(Object message) { + log(Level.DEBUG, message); + } + + public void debug(Object message, Object... params) { + log(Level.DEBUG, message.toString(), params); + } + + public void debug(Object message, Throwable t) { + log(Level.DEBUG, message, t); + } + + public void debug(Object message, Throwable t, Object... params) { + log(Level.DEBUG, message.toString(), t, params); + } + + public void trace(Object message) { + log(Level.TRACE, message); + } + + public void trace(Object message, Object... params) { + log(Level.TRACE, message.toString(), params); + } + + public void trace(Object message, Throwable t) { + log(Level.TRACE, message.toString(), t); + } + + public void trace(Object message, Throwable t, Object... params) { + log(Level.TRACE, message.toString(), t, params); + } + + private void log(Level level, Object message) { + log(level, message, null); + } + + private void log(Level level, Object message, Throwable t) { + if (message instanceof Message) { + delegate.logIfEnabled(FQCN, level, null, (Message) message, t); + } else { + delegate.logIfEnabled(FQCN, level, null, message, t); + } + } + + private void log(Level level, String message, Object... params) { + delegate.logIfEnabled(FQCN, level, null, message, params); + } + + private void log(Level level, String message, Throwable t, Object... params) { + delegate.logIfEnabled(FQCN, level, null, new FormattedMessage(message, params), t); + } +} diff --git a/src/main/java/org/prebid/server/log/LoggerFactory.java b/src/main/java/org/prebid/server/log/LoggerFactory.java new file mode 100644 index 00000000000..7e10b4ac234 --- /dev/null +++ b/src/main/java/org/prebid/server/log/LoggerFactory.java @@ -0,0 +1,22 @@ +package org.prebid.server.log; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.spi.ExtendedLogger; + +public class LoggerFactory { + + private LoggerFactory() { + } + + public static Logger getLogger(Class clazz) { + final String name = clazz.isAnonymousClass() + ? clazz.getEnclosingClass().getCanonicalName() + : clazz.getCanonicalName(); + + return getLogger(name); + } + + public static Logger getLogger(String name) { + return new Logger((ExtendedLogger) LogManager.getLogger(name)); + } +} diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 09467050ff7..fc9d3c251c3 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -62,8 +62,8 @@ public enum MetricName { nobid, gotbids, badinput, - blacklisted_account, - blacklisted_app, + blocklisted_account, + blocklisted_app, badserverresponse, failedtorequestbids, timeout, @@ -147,32 +147,6 @@ public enum MetricName { // price-floors price_floors("price-floors"), - // win notifications - win_notifications, - win_requests, - win_request_preparation_failed, - win_request_time, - win_request_failed, - win_request_successful, - - // user details - user_details_requests, - user_details_request_preparation_failed, - user_details_request_time, - user_details_request_failed, - user_details_request_successful, - - // pg - planner_lineitems_received, - planner_requests, - planner_request_failed, - planner_request_successful, - planner_request_time, - delivery_requests, - delivery_request_failed, - delivery_request_successful, - delivery_request_time, - // activity disallowed_count("disallowed.count"), processed_rules_count("processedrules.count"); diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 8ce8e1b4936..ed11d511f5f 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -3,6 +3,8 @@ import com.codahale.metrics.MetricRegistry; import com.iab.openrtb.request.Imp; import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.hooks.execution.model.ExecutionAction; import org.prebid.server.hooks.execution.model.ExecutionStatus; import org.prebid.server.hooks.execution.model.Stage; @@ -29,7 +31,6 @@ public class Metrics extends UpdatableMetrics { private static final String ALL_REQUEST_BIDDERS = "all"; private final AccountMetricsVerbosityResolver accountMetricsVerbosityResolver; - private final Function requestMetricsCreator; private final Function accountMetricsCreator; private final Function adapterMetricsCreator; @@ -58,7 +59,6 @@ public class Metrics extends UpdatableMetrics { private final CurrencyRatesMetrics currencyRatesMetrics; private final Map settingsCacheMetrics; private final HooksMetrics hooksMetrics; - private final PgMetrics pgMetrics; public Metrics(MetricRegistry metricRegistry, CounterType counterType, @@ -97,7 +97,6 @@ public Metrics(MetricRegistry metricRegistry, currencyRatesMetrics = new CurrencyRatesMetrics(metricRegistry, counterType); settingsCacheMetrics = new HashMap<>(); hooksMetrics = new HooksMetrics(metricRegistry, counterType); - pgMetrics = new PgMetrics(metricRegistry, counterType); } RequestsMetrics requests() { @@ -140,10 +139,6 @@ UserSyncMetrics userSync() { return userSyncMetrics; } - PgMetrics pgMetrics() { - return pgMetrics; - } - CookieSyncMetrics cookieSync() { return cookieSyncMetrics; } @@ -394,47 +389,78 @@ public void updateCookieSyncTcfBlockedMetric(String bidder) { cookieSync().forBidder(bidder).tcf().incCounter(MetricName.blocked); } - public void updateAuctionTcfMetrics(String bidder, - MetricName requestType, - boolean userFpdRemoved, - boolean userIdsRemoved, - boolean geoMasked, - boolean analyticsBlocked, - boolean requestBlocked) { + public void updateAuctionTcfAndLmtMetrics(ActivityInfrastructure activityInfrastructure, + String bidder, + MetricName requestType, + boolean userFpdRemoved, + boolean userIdsRemoved, + boolean geoMasked, + boolean analyticsBlocked, + boolean requestBlocked, + boolean lmtEnabled) { final TcfMetrics tcf = forAdapter(bidder).requestType(requestType).tcf(); - if (userFpdRemoved) { - tcf.incCounter(MetricName.userfpd_masked); + if (lmtEnabled) { + privacy().incCounter(MetricName.lmt); + } + + if (userFpdRemoved || lmtEnabled) { + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_UFPD, ComponentType.BIDDER, bidder); + if (userFpdRemoved) { + tcf.incCounter(MetricName.userfpd_masked); + } } - if (userIdsRemoved) { - tcf.incCounter(MetricName.userid_removed); + if (userIdsRemoved || lmtEnabled) { + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_EIDS, ComponentType.BIDDER, bidder); + if (userIdsRemoved) { + tcf.incCounter(MetricName.userid_removed); + } } - if (geoMasked) { - tcf.incCounter(MetricName.geo_masked); + if (geoMasked || lmtEnabled) { + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_GEO, ComponentType.BIDDER, bidder); + if (geoMasked) { + tcf.incCounter(MetricName.geo_masked); + } } if (analyticsBlocked) { tcf.incCounter(MetricName.analytics_blocked); } if (requestBlocked) { + activityInfrastructure.updateActivityMetrics(Activity.CALL_BIDDER, ComponentType.BIDDER, bidder); tcf.incCounter(MetricName.request_blocked); } } - public void updatePrivacyCoppaMetric() { + public void updatePrivacyCoppaMetric(ActivityInfrastructure activityInfrastructure, Iterable bidders) { privacy().incCounter(MetricName.coppa); - } + bidders.forEach(bidder -> { + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_UFPD, ComponentType.BIDDER, bidder); + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_EIDS, ComponentType.BIDDER, bidder); + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_GEO, ComponentType.BIDDER, bidder); + }); - public void updatePrivacyLmtMetric() { - privacy().incCounter(MetricName.lmt); } - public void updatePrivacyCcpaMetrics(boolean isSpecified, boolean isEnforced) { + public void updatePrivacyCcpaMetrics(ActivityInfrastructure activityInfrastructure, + boolean isSpecified, + boolean isEnforced, + boolean isEnabled, + Iterable bidders) { + if (isSpecified) { privacy().usp().incCounter(MetricName.specified); } if (isEnforced) { privacy().usp().incCounter(MetricName.opt_out); + + if (isEnabled) { + bidders.forEach(bidder -> { + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_UFPD, ComponentType.BIDDER, bidder); + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_EIDS, ComponentType.BIDDER, bidder); + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_GEO, ComponentType.BIDDER, bidder); + }); + } } } @@ -509,58 +535,6 @@ public void createHttpClientCircuitBreakerNumberGauge(LongSupplier numberSupplie forCircuitBreakerType(MetricName.http).createGauge(MetricName.existing, numberSupplier); } - public void updatePlannerRequestMetric(boolean successful) { - pgMetrics().incCounter(MetricName.planner_requests); - if (successful) { - pgMetrics().incCounter(MetricName.planner_request_successful); - } else { - pgMetrics().incCounter(MetricName.planner_request_failed); - } - } - - public void updateDeliveryRequestMetric(boolean successful) { - pgMetrics().incCounter(MetricName.delivery_requests); - if (successful) { - pgMetrics().incCounter(MetricName.delivery_request_successful); - } else { - pgMetrics().incCounter(MetricName.delivery_request_failed); - } - } - - public void updateWinEventRequestMetric(boolean successful) { - incCounter(MetricName.win_requests); - if (successful) { - incCounter(MetricName.win_request_successful); - } else { - incCounter(MetricName.win_request_failed); - } - } - - public void updateUserDetailsRequestMetric(boolean successful) { - incCounter(MetricName.user_details_requests); - if (successful) { - incCounter(MetricName.user_details_request_successful); - } else { - incCounter(MetricName.user_details_request_failed); - } - } - - public void updateWinRequestTime(long millis) { - updateTimer(MetricName.win_request_time, millis); - } - - public void updateLineItemsNumberMetric(long count) { - pgMetrics().incCounter(MetricName.planner_lineitems_received, count); - } - - public void updatePlannerRequestTime(long millis) { - pgMetrics().updateTimer(MetricName.planner_request_time, millis); - } - - public void updateDeliveryRequestTime(long millis) { - pgMetrics().updateTimer(MetricName.delivery_request_time, millis); - } - public void updateGeoLocationMetric(boolean successful) { incCounter(MetricName.geolocation_requests); if (successful) { @@ -700,18 +674,6 @@ static MetricName fromAction(ExecutionAction action) { } } - public void updateWinNotificationMetric() { - incCounter(MetricName.win_notifications); - } - - public void updateWinRequestPreparationFailed() { - incCounter(MetricName.win_request_preparation_failed); - } - - public void updateUserDetailsRequestPreparationFailed() { - incCounter(MetricName.user_details_request_preparation_failed); - } - public void updateRequestsActivityDisallowedCount(Activity activity) { requests().activities().forActivity(activity).incCounter(MetricName.disallowed_count); } diff --git a/src/main/java/org/prebid/server/metric/PgMetrics.java b/src/main/java/org/prebid/server/metric/PgMetrics.java deleted file mode 100644 index a7aca3a35a4..00000000000 --- a/src/main/java/org/prebid/server/metric/PgMetrics.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.metric; - -import com.codahale.metrics.MetricRegistry; - -public class PgMetrics extends UpdatableMetrics { - - PgMetrics(MetricRegistry metricRegistry, CounterType counterType) { - super(metricRegistry, counterType, metricName -> "pg." + metricName); - } -} diff --git a/src/main/java/org/prebid/server/optout/GoogleRecaptchaVerifier.java b/src/main/java/org/prebid/server/optout/GoogleRecaptchaVerifier.java index dcff4bade72..184f7033f7f 100644 --- a/src/main/java/org/prebid/server/optout/GoogleRecaptchaVerifier.java +++ b/src/main/java/org/prebid/server/optout/GoogleRecaptchaVerifier.java @@ -3,15 +3,15 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.vertx.core.Future; import io.vertx.core.MultiMap; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.optout.model.RecaptchaResponse; import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java b/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java index a555f60608b..471eb5715e3 100644 --- a/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java @@ -1,9 +1,9 @@ package org.prebid.server.privacy; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import lombok.experimental.Delegate; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java index 5f2e6df2025..bae5cf4370b 100644 --- a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java +++ b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java @@ -4,13 +4,13 @@ import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.User; import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.privacy.ccpa.Ccpa; import org.prebid.server.privacy.model.Privacy; import org.prebid.server.proto.request.CookieSyncRequest; diff --git a/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java b/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java index 21126b7df0e..9bbb38699f8 100644 --- a/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java +++ b/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java @@ -26,6 +26,9 @@ public boolean isNotEmpty() { } public boolean isEnforced() { + if (usPrivacy == null) { + return false; + } try { validateUsPrivacy(usPrivacy); } catch (PreBidException e) { diff --git a/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java b/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java index f2369e6247f..3134f33e91d 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java @@ -100,21 +100,23 @@ private Future> permissionsForInternal(Collection vendorPermissionsByType = toVendorPermissionsByType(vendorPermissions, accountGdprConfig); - // TODO: always merge account config for purpose1 with next major release return versionedVendorListService.forConsent(tcfConsent) .compose(vendorGvlPermissions -> processSupportedPurposeStrategies( tcfConsent, wrapWithGVL(vendorPermissionsByType, vendorGvlPermissions), mergedPurposes, - purposeOneTreatmentInterpretation), + mergedPurposeOneTreatmentInterpretation), ignored -> processDowngradedSupportedPurposeStrategies( tcfConsent, wrapWithGVL(vendorPermissionsByType, Collections.emptyMap()), mergedPurposes, - mergePurposeOneTreatmentInterpretation(accountGdprConfig))) + mergedPurposeOneTreatmentInterpretation)) .map(ignored -> enforcePurpose4IfRequired(mergedPurposes, vendorPermissionsByType)) .map(ignored -> processSupportedSpecialFeatureStrategies( tcfConsent, diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 0e3aba69dfa..b02698bbce3 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -2,19 +2,19 @@ import com.iabtcf.decoder.TCString; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import lombok.Value; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.execution.Timeout; -import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; @@ -27,12 +27,15 @@ import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.GdprConfig; +import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -59,18 +62,20 @@ public class TcfDefinerService { private final boolean consentStringMeansInScope; private final Tcf2Service tcf2Service; private final Set eeaCountries; - private final GeoLocationService geoLocationService; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; private final BidderCatalog bidderCatalog; private final IpAddressHelper ipAddressHelper; private final Metrics metrics; + private final double samplingRate; public TcfDefinerService(GdprConfig gdprConfig, Set eeaCountries, Tcf2Service tcf2Service, - GeoLocationService geoLocationService, + GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, - Metrics metrics) { + Metrics metrics, + double samplingRate) { this.gdprEnabled = gdprConfig != null && BooleanUtils.isNotFalse(gdprConfig.getEnabled()); this.gdprDefaultValue = gdprConfig != null ? gdprConfig.getDefaultValue() : null; @@ -78,10 +83,11 @@ public TcfDefinerService(GdprConfig gdprConfig, && BooleanUtils.isTrue(gdprConfig.getConsentStringMeansInScope()); this.tcf2Service = Objects.requireNonNull(tcf2Service); this.eeaCountries = Objects.requireNonNull(eeaCountries); - this.geoLocationService = geoLocationService; + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; } /** @@ -93,11 +99,12 @@ public Future resolveTcfContext(Privacy privacy, AccountGdprConfig accountGdprConfig, MetricName requestType, RequestLogInfo requestLogInfo, - Timeout timeout) { + Timeout timeout, + GeoInfo geoInfo) { final Future tcfContextFuture = !isGdprEnabled(accountGdprConfig, requestType) ? Future.succeededFuture(TcfContext.empty()) - : prepareTcfContext(privacy, country, ipAddress, requestLogInfo, timeout); + : prepareTcfContext(privacy, country, ipAddress, accountGdprConfig, requestLogInfo, timeout, geoInfo); return tcfContextFuture.map(this::updateTcfGeoMetrics); } @@ -112,7 +119,15 @@ public Future resolveTcfContext(Privacy privacy, RequestLogInfo requestLogInfo, Timeout timeout) { - return resolveTcfContext(privacy, null, ipAddress, accountGdprConfig, requestType, requestLogInfo, timeout); + return resolveTcfContext( + privacy, + null, + ipAddress, + accountGdprConfig, + requestType, + requestLogInfo, + timeout, + null); } public Future> resultForVendorIds(Set vendorIds, TcfContext tcfContext) { @@ -175,8 +190,10 @@ private boolean isGdprEnabled(AccountGdprConfig accountGdprConfig, MetricName re private Future prepareTcfContext(Privacy privacy, String country, String ipAddress, + AccountGdprConfig accountGdprConfig, RequestLogInfo requestLogInfo, - Timeout timeout) { + Timeout timeout, + GeoInfo geoInfo) { final String consentString = privacy.getConsentString(); final TCStringParsingResult consentStringParsingResult = parseConsentString(consentString, requestLogInfo); @@ -184,7 +201,7 @@ private Future prepareTcfContext(Privacy privacy, final boolean consentValid = isConsentValid(consent); final String effectiveIpAddress = maybeMaskIp(ipAddress, consent); - final Boolean inEea = isCountryInEea(country); + final Boolean inEea = isCountryInEea(country, accountGdprConfig); final TcfContext defaultContext = TcfContext.builder() .inGdprScope(inScopeOfGdpr(gdprDefaultValue)) @@ -205,17 +222,9 @@ private Future prepareTcfContext(Privacy privacy, return Future.succeededFuture(defaultContext.toBuilder().inGdprScope(inScopeOfGdpr(gdpr)).build()); } - if (country != null) { - return Future.succeededFuture(defaultContext.toBuilder().inGdprScope(inScopeOfGdpr(inEea)).build()); - } - - if (ipAddress != null && geoLocationService != null) { - return geoLocationService.lookup(effectiveIpAddress, timeout) - .map(geoInfo -> updateMetricsAndEnrichWithGeo(geoInfo, defaultContext)) - .recover(error -> logError(error, defaultContext)); - } - - return Future.succeededFuture(defaultContext); + return geoLocationServiceWrapper.doLookup(effectiveIpAddress, country, timeout) + .recover(ignored -> Future.succeededFuture(geoInfo)) + .map(lookupResult -> enrichWithGeoInfo(defaultContext, lookupResult, country, accountGdprConfig)); } private String maybeMaskIp(String ipAddress, TCString consent) { @@ -237,30 +246,35 @@ private static boolean shouldMaskIp(TCString consent) { return isConsentValid(consent) && consent.getVersion() == 2 && !consent.getSpecialFeatureOptIns().contains(1); } - private TcfContext updateMetricsAndEnrichWithGeo(GeoInfo geoInfo, TcfContext tcfContext) { - metrics.updateGeoLocationMetric(true); - final Boolean inEea = isCountryInEea(geoInfo.getCountry()); + private TcfContext enrichWithGeoInfo(TcfContext defaultTcfContext, + GeoInfo geoInfo, + String defaultCountry, + AccountGdprConfig accountGdprConfig) { + + final String country = ObjectUtil.getIfNotNullOrDefault(geoInfo, GeoInfo::getCountry, () -> defaultCountry); + final Boolean inEea = isCountryInEea(country, accountGdprConfig); final boolean inScope = inScopeOfGdpr(inEea); - return tcfContext.toBuilder() - .geoInfo(geoInfo) - .inGdprScope(inScope) + return defaultTcfContext.toBuilder() .inEea(inEea) + .inGdprScope(inScope) + .geoInfo(geoInfo) .build(); } - private Future logError(Throwable error, TcfContext tcfContext) { - final String message = "Geolocation lookup failed: " + error.getMessage(); - logger.warn(message); - logger.debug(message, error); - - metrics.updateGeoLocationMetric(false); - - return Future.succeededFuture(tcfContext); + private Boolean isCountryInEea(String country, AccountGdprConfig accountGdprConfig) { + final Set publisherEeaCountries = Optional.ofNullable(accountGdprConfig) + .map(AccountGdprConfig::getEeaCountries) + .map(TcfDefinerService::eeaCountries) + .orElse(eeaCountries); + return country != null ? publisherEeaCountries.contains(country) : null; } - private Boolean isCountryInEea(String country) { - return country != null ? eeaCountries.contains(country) : null; + private static Set eeaCountries(String eeaCountriesAsString) { + return Arrays.stream(eeaCountriesAsString.split(",")) + .map(StringUtils::strip) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); } private TcfContext updateTcfGeoMetrics(TcfContext tcfContext) { @@ -349,11 +363,14 @@ private TCStringParsingResult toValidResult(String consentString, TCStringParsin } final int tcfPolicyVersion = tcString.getTcfPolicyVersion(); - // disable support for tcf policy version > 4 - if (tcfPolicyVersion > 4) { - warnings.add("Parsing consent string: %s failed. TCF policy version %d is not supported".formatted( - consentString, tcfPolicyVersion)); - return TCStringParsingResult.of(TCStringEmpty.create(), warnings); + // support for tcf policy version > 5 + if (tcfPolicyVersion > 5) { + metrics.updateAlertsMetrics(MetricName.general); + + final String message = "Unknown tcfPolicyVersion %s, defaulting to gvlSpecificationVersion=3" + .formatted(tcfPolicyVersion); + UNDEFINED_CORRUPT_CONSENT_LOGGER.warn(message, samplingRate); + warnings.add(message); } return TCStringParsingResult.of(tcString, warnings); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java index 8324d606e79..ce9fcd2dc70 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java @@ -1,8 +1,8 @@ package org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies; import com.iabtcf.decoder.TCString; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; @@ -20,7 +20,7 @@ public Stream allowedByTypeStrategy(PurposeCode purpose, Collection excludedVendors, boolean isEnforceVendors) { - logger.debug("Basic strategy used for purpose {0}", purpose); + logger.debug("Basic strategy used for purpose {}", purpose); final Stream allowedVendorPermissions = toVendorPermissions(vendorsForPurpose) .filter(vendorPermission -> vendorPermission.getVendorId() != null) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java index 019da5e36b4..671bdb39170 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java @@ -9,8 +9,6 @@ import io.vertx.core.file.FileProps; import io.vertx.core.file.FileSystem; import io.vertx.core.file.FileSystemException; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import lombok.AllArgsConstructor; import lombok.Value; import org.apache.commons.collections4.MapUtils; @@ -19,11 +17,13 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.vendorlist.proto.Vendor; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorList; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.io.File; import java.io.IOException; @@ -153,7 +153,7 @@ public Future> forVersion(int version) { metrics.updatePrivacyTcfVendorListMissingMetric(tcf); if (fetchThrottler.registerFetchAttempt(version)) { - logger.info("TCF {0} vendor list for version {1}.{2} not found, started downloading.", + logger.info("TCF {} vendor list for version {}.{} not found, started downloading.", tcf, generationVersion, version); fetchNewVendorListFor(version); } @@ -346,7 +346,7 @@ private Void updateCache(VendorListResult vendorListResult) { metrics.updatePrivacyTcfVendorListOkMetric(tcf); - logger.info("Created new TCF {0} vendor list for version {1}.{2}", tcf, generationVersion, version); + logger.info("Created new TCF {} vendor list for version {}.{}", tcf, generationVersion, version); stopUsingFallbackForVersion(version); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java index d6375872383..5e261d9b6b4 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java @@ -2,7 +2,6 @@ import com.iabtcf.decoder.TCString; import io.vertx.core.Future; -import org.prebid.server.exception.PreBidException; import org.prebid.server.privacy.gdpr.vendorlist.proto.Vendor; import java.util.Map; @@ -21,12 +20,9 @@ public VersionedVendorListService(VendorListService vendorListServiceV2, VendorL public Future> forConsent(TCString consent) { final int tcfPolicyVersion = consent.getTcfPolicyVersion(); final int vendorListVersion = consent.getVendorListVersion(); - if (tcfPolicyVersion < 4) { - return vendorListServiceV2.forVersion(vendorListVersion); - } else if (tcfPolicyVersion == 4) { - return vendorListServiceV3.forVersion(vendorListVersion); - } - return Future.failedFuture(new PreBidException("Invalid tcf policy version: %d".formatted(tcfPolicyVersion))); + return tcfPolicyVersion < 4 + ? vendorListServiceV2.forVersion(vendorListVersion) + : vendorListServiceV3.forVersion(vendorListVersion); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaPublisherRender.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaPublisherRender.java new file mode 100644 index 00000000000..e507bd19f67 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaPublisherRender.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request; + +public enum DsaPublisherRender { + + NOT_RENDER(0), + COULD_RENDER(1), + WILL_RENDER(2); + + private final int value; + + DsaPublisherRender(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaRequired.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaRequired.java new file mode 100644 index 00000000000..e2f7a29a652 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaRequired.java @@ -0,0 +1,19 @@ +package org.prebid.server.proto.openrtb.ext.request; + +public enum DsaRequired { + + NOT_REQUIRED(0), + SUPPORTED(1), + REQUIRED(2), + REQUIRED_ONLINE_PLATFORM(3); + + private final int value; + + DsaRequired(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaTransparency.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaTransparency.java new file mode 100644 index 00000000000..6b2e47bc623 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/DsaTransparency.java @@ -0,0 +1,27 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i] + * and bidresponse.seatbid[i].bid[i].ext.dsa.transparency[i] + */ +@Value(staticConstructor = "of") +public class DsaTransparency { + + /** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].domain + * and bidresponse.seatbid[i].bid[i].ext.dsa.transparency[i].domain + */ + String domain; + + /** + * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].dsaparams[] + * and bidresponse.seatbid[i].bid[i].ext.dsa.transparency[i].dsaparams[] + */ + @JsonProperty("dsaparams") + List dsaParams; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java index f80f925cd5f..fd6206cb90d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java @@ -56,4 +56,9 @@ public class ExtImpPrebid { * Defines the contract for bidrequest.imp[i].ext.prebid.passthrough */ JsonNode passthrough; + + /** + * Defines the contract for bidrequest.imp[i].ext.prebid.imp + */ + ObjectNode imp; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java index 3dc3211097f..c976b523e5e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsa.java @@ -32,6 +32,6 @@ public class ExtRegsDsa { /** * Defines the contract for bidrequest.regs.ext.dsa.transparency[] */ - List transparency; + List transparency; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java deleted file mode 100644 index 6542326bd5c..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRegsDsaTransparency.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -import java.util.List; - -/** - * Defines the contract for bidrequest.regs.ext.dsa.transparency[i] - */ -@Value(staticConstructor = "of") -public class ExtRegsDsaTransparency { - - /** - * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].domain - */ - String domain; - - /** - * Defines the contract for bidrequest.regs.ext.dsa.transparency[i].dsaparams[] - */ - @JsonProperty("dsaparams") - List dsaParams; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 0e7005b81b8..6dee1b7ba38 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -70,6 +70,12 @@ public class ExtRequestPrebid { */ ExtStoredRequest storedrequest; + /** + * Defines the contract for bidrequest.ext.prebid.storedauctionresponse + */ + @JsonProperty("storedauctionresponse") + ExtStoredAuctionResponse storedAuctionResponse; + /** * Defines the contract for bidrequest.ext.prebid.cache */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java index 5a0360f587f..e8f83afcccb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java @@ -1,11 +1,21 @@ package org.prebid.server.proto.openrtb.ext.request; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.response.SeatBid; import lombok.AllArgsConstructor; import lombok.Value; +import java.util.List; + @AllArgsConstructor(staticName = "of") @Value public class ExtStoredAuctionResponse { String id; + + @JsonProperty("seatbidarr") + List seatBids; + + @JsonProperty("seatbidobj") + SeatBid seatBid; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/admatic/AdmaticImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/admatic/AdmaticImpExt.java new file mode 100644 index 00000000000..7976572f73a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/admatic/AdmaticImpExt.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.admatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AdmaticImpExt { + + @JsonProperty("host") + String host; + + @JsonProperty("networkId") + Integer networkId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java index 04105dd73d8..268ccd077ec 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java @@ -1,39 +1,22 @@ package org.prebid.server.proto.openrtb.ext.request.adtarget; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -/** - * Defines the contract for bidrequest.imp[i].ext.adtarget - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdtarget { - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.aid - */ @JsonProperty("aid") - Integer sourceId; + String sourceId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.placementId - */ @JsonProperty("placementId") Integer placementId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.siteId - */ @JsonProperty("siteId") Integer siteId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.bidFloor - */ @JsonProperty("bidFloor") BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java index 907932dd44b..00df38b9533 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java @@ -1,39 +1,22 @@ package org.prebid.server.proto.openrtb.ext.request.adtelligent; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -/** - * Defines the contract for bidrequest.imp[i].ext.adtelligent - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdtelligent { - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.aid - */ @JsonProperty("aid") - Integer sourceId; + String sourceId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.placementId - */ @JsonProperty("placementId") Integer placementId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.siteId - */ @JsonProperty("siteId") Integer siteId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.bidFloor - */ @JsonProperty("bidFloor") BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java new file mode 100644 index 00000000000..121d025f654 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.adtonos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdtonos { + + @JsonProperty("supplierId") + String supplierId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/aso/ExtImpAso.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aso/ExtImpAso.java new file mode 100644 index 00000000000..94eacc87567 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aso/ExtImpAso.java @@ -0,0 +1,10 @@ +package org.prebid.server.proto.openrtb.ext.request.aso; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAso { + + Integer zone; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java new file mode 100644 index 00000000000..5823f02551e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request.bidmatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpBidmatic { + + @JsonProperty("source") + String sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bigoad/ExtImpBigoad.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bigoad/ExtImpBigoad.java new file mode 100644 index 00000000000..c9377ca3bb2 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bigoad/ExtImpBigoad.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.bigoad; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBigoad { + + @JsonProperty("sspid") + String sspId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java deleted file mode 100644 index 588137321e3..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request.bizzclick; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -@Value(staticConstructor = "of") -public class ExtImpBizzclick { - - @JsonProperty("accountId") - String accountId; - - @JsonProperty("placementId") - String placementId; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java new file mode 100644 index 00000000000..99413fa5e40 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java @@ -0,0 +1,20 @@ +package org.prebid.server.proto.openrtb.ext.request.blasto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBlasto { + + @JsonProperty("host") + String host; + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("sourceId") + String sourceId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bwx/ExtImpBwx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bwx/ExtImpBwx.java new file mode 100644 index 00000000000..ceb53dde28b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bwx/ExtImpBwx.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.bwx; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBwx { + + String env; + + String pid; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/concert/ExtImpConcert.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/concert/ExtImpConcert.java new file mode 100644 index 00000000000..b90732b55fb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/concert/ExtImpConcert.java @@ -0,0 +1,24 @@ +package org.prebid.server.proto.openrtb.ext.request.concert; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value(staticConstructor = "of") +public class ExtImpConcert { + + @JsonProperty("partnerId") + String partnerId; + + @JsonProperty("placementId") + Integer placementId; + + String site; + + String slot; + + List> sizes; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java index e9e852d9099..a88bbc2194d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java @@ -11,10 +11,11 @@ public class ExtImpConnectAd { @JsonProperty("networkId") - Integer networkId; + String networkId; @JsonProperty("siteId") - Integer siteId; + String siteId; - BigDecimal bidfloor; + @JsonProperty("bidfloor") + BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/consumable/ExtImpConsumable.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/consumable/ExtImpConsumable.java index bc4d479f443..e9590dc2b17 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/consumable/ExtImpConsumable.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/consumable/ExtImpConsumable.java @@ -22,4 +22,7 @@ public class ExtImpConsumable { @JsonProperty("unitName") String unitName; + + @JsonProperty("placementId") + String placementId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java new file mode 100644 index 00000000000..1c2c057878a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.copper6ssp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ImpExtCopper6Ssp { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} + diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/displayio/DisplayioImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/displayio/DisplayioImpExt.java new file mode 100644 index 00000000000..a2e500ce310 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/displayio/DisplayioImpExt.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request.displayio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class DisplayioImpExt { + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("inventoryId") + String inventoryId; + + @JsonProperty("placementId") + String placementId; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/driftpixel/DriftpixelImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/driftpixel/DriftpixelImpExt.java new file mode 100644 index 00000000000..e254667046e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/driftpixel/DriftpixelImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.driftpixel; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class DriftpixelImpExt { + + String env; + + String pid; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java new file mode 100644 index 00000000000..03b14ab82eb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.escalax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpEscalax { + + @JsonProperty("sourceId") + String sourceId; + + @JsonProperty("accountId") + String accountId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java deleted file mode 100644 index 5232d38c985..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request.liftoff; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class ExtImpLiftoff { - - String bidToken; - - String appStoreId; - - String placementReferenceId; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java index f572890ab72..b07a6741019 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java @@ -1,16 +1,18 @@ package org.prebid.server.proto.openrtb.ext.request.loopme; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -/** - * Defines the contract for bidrequest.imp[i].ext.loopme - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpLoopme { - @JsonProperty("accountId") - String accountId; + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("bundleId") + String bundleId; + + @JsonProperty("placementId") + String placementId; + } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loyal/ExtImpLoyal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loyal/ExtImpLoyal.java new file mode 100644 index 00000000000..034eeb29476 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loyal/ExtImpLoyal.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.loyal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpLoyal { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediago/MediaGoImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediago/MediaGoImpExt.java new file mode 100644 index 00000000000..3baba30b2f0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediago/MediaGoImpExt.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.mediago; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediaGoImpExt { + + String token; + + String region; + + @JsonProperty("placementId") + String placementId; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java new file mode 100644 index 00000000000..ae7b45e8c86 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.melozen; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MeloZenImpExt { + + @JsonProperty("pubId") + String pubId; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java new file mode 100644 index 00000000000..3101cb8a214 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.metax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMetax { + + @JsonProperty("publisherId") + Integer publisherId; + + @JsonProperty("adunit") + Integer adUnit; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java new file mode 100644 index 00000000000..016e6ca99d5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMissena { + + @JsonProperty("apiKey") + String apiKey; + + String placement; + + @JsonProperty("test") + String testMode; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/openweb/ExtImpOpenweb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/openweb/ExtImpOpenweb.java index c4efc42005d..a55ffea8b83 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/openweb/ExtImpOpenweb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/openweb/ExtImpOpenweb.java @@ -4,21 +4,14 @@ import lombok.AllArgsConstructor; import lombok.Value; -import java.math.BigDecimal; - @Value @AllArgsConstructor(staticName = "of") public class ExtImpOpenweb { - @JsonProperty("aid") - Integer sourceId; - - @JsonProperty("placementId") - Integer placementId; + Integer aid; - @JsonProperty("siteId") - Integer siteId; + String org; - @JsonProperty("bidFloor") - BigDecimal bidFloor; + @JsonProperty("placementId") + String placementId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/openx/ExtImpOpenx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/openx/ExtImpOpenx.java index 862eb4289ca..0bf40d18264 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/openx/ExtImpOpenx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/openx/ExtImpOpenx.java @@ -1,5 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request.openx; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import lombok.Builder; @@ -22,6 +23,7 @@ public class ExtImpOpenx { String platform; + @JsonFormat(shape = JsonFormat.Shape.STRING) @JsonProperty("customFloor") BigDecimal customFloor; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java new file mode 100644 index 00000000000..860ddb430d9 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.oraki; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOraki { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java new file mode 100644 index 00000000000..6206f540b92 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.ownadx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOwnAdx { + + @JsonProperty("sspId") + String sspId; + + @JsonProperty("seatId") + String seatId; + + @JsonProperty("tokenId") + String tokenId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/playdigo/ExtImpPlaydigo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/playdigo/ExtImpPlaydigo.java new file mode 100644 index 00000000000..e15f82a9c2d --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/playdigo/ExtImpPlaydigo.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.playdigo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpPlaydigo { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/playdigo/PlaydigoImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/playdigo/PlaydigoImpExt.java new file mode 100644 index 00000000000..f46c3d5a17a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/playdigo/PlaydigoImpExt.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request.playdigo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class PlaydigoImpExt { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java new file mode 100644 index 00000000000..6cac7640123 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.pubrise; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpPubrise { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java new file mode 100644 index 00000000000..0f5df5f144d --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.qt; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpQt { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/readpeak/ExtImpReadPeak.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/readpeak/ExtImpReadPeak.java new file mode 100644 index 00000000000..5ed1e14d64c --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/readpeak/ExtImpReadPeak.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request.readpeak; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpReadPeak { + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("siteId") + String siteId; + + @JsonProperty("bidfloor") + BigDecimal bidFloor; + + @JsonProperty("tagId") + String tagId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rise/ExtImpRise.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rise/ExtImpRise.java index 3a3bf301a56..251eb0bc9e9 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rise/ExtImpRise.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rise/ExtImpRise.java @@ -8,4 +8,6 @@ public class ExtImpRise { String publisherId; String org; + + String placementId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/roulax/ExtImpRoulax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/roulax/ExtImpRoulax.java new file mode 100644 index 00000000000..11d69e39674 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/roulax/ExtImpRoulax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.roulax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpRoulax { + + @JsonProperty("PublisherPath") + String publisherPath; + + @JsonProperty("Pid") + String pid; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java index cf2355bf65c..c1cd0e3e326 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java @@ -36,8 +36,6 @@ public class ExtImpRubicon { RubiconVideoParams video; - String pchain; - List keywords; Set formats; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedingalliance/ExtImpSeedingAlliance.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedingalliance/ExtImpSeedingAlliance.java index 58edc92d859..e713106bf38 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedingalliance/ExtImpSeedingAlliance.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedingalliance/ExtImpSeedingAlliance.java @@ -12,4 +12,7 @@ public class ExtImpSeedingAlliance { @JsonProperty("seatId") String seatId; + @JsonProperty("accountId") + String accountId; + } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smrtconnect/ExtImpSmrtconnect.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smrtconnect/ExtImpSmrtconnect.java new file mode 100644 index 00000000000..7dda6c6fa7b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smrtconnect/ExtImpSmrtconnect.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.smrtconnect; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSmrtconnect { + + String supplyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/theadx/ExtImpTheadx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/theadx/ExtImpTheadx.java new file mode 100644 index 00000000000..ff7eb9e86f0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/theadx/ExtImpTheadx.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request.theadx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTheadx { + + @JsonProperty("tagid") + String tagId; + + @JsonProperty("wid") + Integer inventorySourceId; + + @JsonProperty("pid") + Integer memberId; + + @JsonProperty("pname") + String placementName; + +} + diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java new file mode 100644 index 00000000000..ee3b9cc7d83 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.thetradedesk; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTheTradeDesk { + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java new file mode 100644 index 00000000000..5f20441f9cd --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.tradplus; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTradPlus { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/vidazoo/VidazooImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vidazoo/VidazooImpExt.java new file mode 100644 index 00000000000..b63afbd4ad6 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vidazoo/VidazooImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.vidazoo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class VidazooImpExt { + + @JsonProperty("cId") + String connectionId; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java new file mode 100644 index 00000000000..2b6fdfadae4 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.vungle; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpVungle { + + String bidToken; + + String appStoreId; + + String placementReferenceId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java index 28a4e036904..db165d6dd4e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java @@ -6,9 +6,6 @@ import java.util.Map; -/** - * Defines the contract for bidrequest.imp[i].ext.yieldlab - */ @Builder @Value public class ExtImpYieldlab { @@ -19,9 +16,6 @@ public class ExtImpYieldlab { @JsonProperty("supplyId") String supplyId; - @JsonProperty("adSize") - String adSize; - Map targeting; @JsonProperty("extId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zmaticoo/ExtImpZMaticoo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zmaticoo/ExtImpZMaticoo.java new file mode 100644 index 00000000000..321a136d0ac --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zmaticoo/ExtImpZMaticoo.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.zmaticoo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpZMaticoo { + + @JsonProperty("pubId") + String pubId; + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/DsaAdvertiserRender.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/DsaAdvertiserRender.java new file mode 100644 index 00000000000..9b6137ef055 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/DsaAdvertiserRender.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.response; + +public enum DsaAdvertiserRender { + + NOT_RENDER(0), + WILL_RENDER(1); + + private final int value; + + DsaAdvertiserRender(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAnalytics.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAnalytics.java new file mode 100644 index 00000000000..9843ddd336b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAnalytics.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtAnalytics { + + List tags; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAnalyticsTags.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAnalyticsTags.java new file mode 100644 index 00000000000..33d9e19ea71 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAnalyticsTags.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.hooks.execution.model.Stage; + +@Value(staticConstructor = "of") +public class ExtAnalyticsTags { + + Stage stage; + + String module; + + @JsonProperty("analyticstags") + ExtModulesTraceAnalyticsTags analyticsTags; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java new file mode 100644 index 00000000000..70683a8a7a7 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class ExtBidDsa { + + String behalf; + + String paid; + + List transparency; + + @JsonProperty("adrender") + Integer adRender; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidVideo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidVideo.java index 368d690e8c7..8819e5be64f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidVideo.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidVideo.java @@ -1,13 +1,8 @@ package org.prebid.server.proto.openrtb.ext.response; -import lombok.AllArgsConstructor; import lombok.Value; -/** - * Defines the contract for bidresponse.seatbid.bid[i].ext.prebid.video - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtBidPrebidVideo { Integer duration; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java index 2feb2b8edb8..fbf368e5e97 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java @@ -23,6 +23,8 @@ public class ExtBidResponsePrebid { */ ExtModules modules; + ExtAnalytics analytics; + /** * FLEDGE response as bidresponse.ext.prebid.fledge.auctionconfigs[] */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugPgmetrics.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugPgmetrics.java deleted file mode 100644 index bf87b233962..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugPgmetrics.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.response; - -import lombok.Builder; -import lombok.Value; - -import java.util.Map; -import java.util.Set; - -/** - * Defines the contract for bidresponse.ext.debug.pgmetrics - */ -@Builder -@Value -public class ExtDebugPgmetrics { - - public static final ExtDebugPgmetrics EMPTY = ExtDebugPgmetrics.builder().build(); - - Set sentToClient; - - Set sentToClientAsTopMatch; - - Set matchedDomainTargeting; - - Set matchedWholeTargeting; - - Set matchedTargetingFcapped; - - Set matchedTargetingFcapLookupFailed; - - Set readyToServe; - - Set pacingDeferred; - - Map> sentToBidder; - - Map> sentToBidderAsTopMatch; - - Map> receivedFromBidder; - - Set responseInvalidated; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java index e1b047beac1..f17c2c9cf16 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.response; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -import java.util.Map; /** * Defines the contract for bidresponse.ext.debug.trace @@ -14,17 +12,5 @@ @Value public class ExtDebugTrace { - /** - * Defines the contract for bidresponse.ext.debug.trace.deals - */ - List deals; - - /** - * Defines the contract for bidresponse.ext.debug.trace.lineItems - */ - - @JsonProperty("lineitems") - Map> lineItems; - List activityInfrastructure; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java index 253a6d9f7ea..7d1a5f4c499 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java @@ -24,11 +24,6 @@ public class ExtResponseDebug { */ BidRequest resolvedrequest; - /** - * Defines the contract for bidresponse.ext.debug.pgmetrics - */ - ExtDebugPgmetrics pgmetrics; - /** * Defines the contract for bidresponse.ext.debug.trace */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtTraceDeal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtTraceDeal.java deleted file mode 100644 index 1187ef36757..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtTraceDeal.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.response; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.time.ZonedDateTime; - -/** - * Defines the contract for bidresponse.ext.debug.trace.deals[] - */ -@AllArgsConstructor(staticName = "of") -@Value -public class ExtTraceDeal { - - /** - * Defines the contract for bidresponse.ext.debug.trace.deals[].lineitemid - */ - @JsonProperty("lineitemid") - String lineItemId; - - /** - * Defines the contract for bidresponse.ext.debug.trace.deals[].time - */ - ZonedDateTime time; - - /** - * Defines the contract for bidresponse.ext.debug.trace.deals[].category - */ - Category category; - - /** - * Defines the contract for bidresponse.ext.debug.trace.deals[].message - */ - String message; - - public enum Category { - targeting, pacing, cleanup, post_processing - } -} diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index 8a010991b2f..da414bef279 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -14,7 +14,7 @@ * stored requests and imps) from the source. * * @see FileApplicationSettings - * @see JdbcApplicationSettings + * @see DatabaseApplicationSettings * @see HttpApplicationSettings * @see CachingApplicationSettings * @see CompositeApplicationSettings diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 297c19ac2af..97348e7bbd8 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -1,11 +1,11 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.helper.StoredDataFetcher; @@ -48,16 +48,21 @@ public CachingApplicationSettings(ApplicationSettings delegate, SettingsCache videoCache, Metrics metrics, int ttl, - int size) { + int size, + int jitter) { if (ttl <= 0 || size <= 0) { throw new IllegalArgumentException("ttl and size must be positive"); } + if (jitter < 0 || jitter >= ttl) { + throw new IllegalArgumentException("jitter must match the inequality: 0 <= jitter < ttl"); + } + this.delegate = Objects.requireNonNull(delegate); - this.accountCache = SettingsCache.createCache(ttl, size); - this.accountToErrorCache = SettingsCache.createCache(ttl, size); - this.adServerPublisherToErrorCache = SettingsCache.createCache(ttl, size); - this.categoryConfigCache = SettingsCache.createCache(ttl, size); + this.accountCache = SettingsCache.createCache(ttl, size, jitter); + this.accountToErrorCache = SettingsCache.createCache(ttl, size, jitter); + this.adServerPublisherToErrorCache = SettingsCache.createCache(ttl, size, jitter); + this.categoryConfigCache = SettingsCache.createCache(ttl, size, jitter); this.cache = Objects.requireNonNull(cache); this.ampCache = Objects.requireNonNull(ampCache); this.videoCache = Objects.requireNonNull(videoCache); @@ -72,7 +77,7 @@ public Future getAccountById(String accountId, Timeout timeout) { return getFromCacheOrDelegate( accountCache, accountToErrorCache, - accountId, + StringUtils.isBlank(accountId) ? StringUtils.EMPTY : accountId, timeout, delegate::getAccountById, event -> metrics.updateSettingsCacheEventMetric(MetricName.account, event)); @@ -243,12 +248,8 @@ private static Map getFromCacheOrAddMissedIds(String accountId, public void invalidateAccountCache(String accountId) { accountCache.remove(accountId); - logger.debug("Account with id {0} was invalidated", accountId); - } - - public void invalidateAllAccountCache() { - accountCache.clear(); - logger.debug("All accounts cache were invalidated"); + accountToErrorCache.remove(accountId); + logger.debug("Account with id {} was invalidated", accountId); } private static void noOp(ANY any) { diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java new file mode 100644 index 00000000000..9dad7f6a28b --- /dev/null +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -0,0 +1,234 @@ +package org.prebid.server.settings; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.helper.DatabaseStoredDataResultMapper; +import org.prebid.server.settings.helper.DatabaseStoredResponseResultMapper; +import org.prebid.server.settings.helper.ParametrizedQueryHelper; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; +import org.prebid.server.vertx.database.DatabaseClient; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.IntStream; + +/** + * Implementation of {@link ApplicationSettings}. + *

+ * Reads an application settings from the database source. + *

+ * In order to enable caching and reduce latency for read operations {@link DatabaseApplicationSettings} + * can be decorated by {@link CachingApplicationSettings}. + */ +public class DatabaseApplicationSettings implements ApplicationSettings { + + private final DatabaseClient databaseClient; + private final JacksonMapper mapper; + private final ParametrizedQueryHelper parametrizedQueryHelper; + + /** + * Query to select account by ids. + */ + private final String selectAccountQuery; + + /** + * Query to select stored requests and imps by ids, for example: + *

+     * SELECT accountId, reqid, requestData, 'request' as dataType
+     *   FROM stored_requests
+     *   WHERE reqid in (%REQUEST_ID_LIST%)
+     * UNION ALL
+     * SELECT accountId, impid, impData, 'imp' as dataType
+     *   FROM stored_imps
+     *   WHERE impid in (%IMP_ID_LIST%)
+     * 
+ */ + private final String selectStoredRequestsQuery; + + /** + * Query to select amp stored requests by ids, for example: + *
+     * SELECT accountId, reqid, requestData, 'request' as dataType
+     *   FROM stored_requests
+     *   WHERE reqid in (%REQUEST_ID_LIST%)
+     * 
+ */ + private final String selectAmpStoredRequestsQuery; + + /** + * Query to select stored responses by ids, for example: + *
+     * SELECT respid, responseData
+     *   FROM stored_responses
+     *   WHERE respid in (%RESPONSE_ID_LIST%)
+     * 
+ */ + private final String selectStoredResponsesQuery; + + public DatabaseApplicationSettings(DatabaseClient databaseClient, + JacksonMapper mapper, + ParametrizedQueryHelper parametrizedQueryHelper, + String selectAccountQuery, + String selectStoredRequestsQuery, + String selectAmpStoredRequestsQuery, + String selectStoredResponsesQuery) { + + this.databaseClient = Objects.requireNonNull(databaseClient); + this.mapper = Objects.requireNonNull(mapper); + this.parametrizedQueryHelper = Objects.requireNonNull(parametrizedQueryHelper); + this.selectAccountQuery = parametrizedQueryHelper.replaceAccountIdPlaceholder( + Objects.requireNonNull(selectAccountQuery)); + this.selectStoredRequestsQuery = Objects.requireNonNull(selectStoredRequestsQuery); + this.selectAmpStoredRequestsQuery = Objects.requireNonNull(selectAmpStoredRequestsQuery); + this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery); + } + + /** + * Runs a process to get account by id from database + * and returns {@link Future}<{@link Account}>. + */ + @Override + public Future getAccountById(String accountId, Timeout timeout) { + return databaseClient.executeQuery( + selectAccountQuery, + Collections.singletonList(accountId), + result -> mapToModelOrError(result, this::toAccount), + timeout) + .compose(result -> failedIfNull(result, accountId, "Account")); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return Future.failedFuture(new PreBidException("Not supported")); + } + + /** + * Transforms the first row of {@link RowSet} to required object or returns null. + *

+ * Note: mapper should never throws exception in case of using + * {@link CircuitBreakerSecuredDatabaseClient}. + */ + private T mapToModelOrError(RowSet rowSet, Function mapper) { + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + return rowIterator != null && rowIterator.hasNext() + ? mapper.apply(rowIterator.next()) + : null; + } + + /** + * Returns succeeded {@link Future} if given value is not equal to NULL, + * otherwise failed {@link Future} with {@link PreBidException}. + */ + private static Future failedIfNull(T value, String id, String errorPrefix) { + return value != null + ? Future.succeededFuture(value) + : Future.failedFuture(new PreBidException("%s not found: %s".formatted(errorPrefix, id))); + } + + private Account toAccount(Row row) { + final String source = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); + try { + return source != null ? mapper.decodeValue(source, Account.class) : null; + } catch (DecodeException e) { + throw new PreBidException(e.getMessage()); + } + } + + /** + * Runs a process to get stored requests by a collection of ids from database + * and returns {@link Future}<{@link StoredDataResult}>. + */ + @Override + public Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); + } + + /** + * Runs a process to get stored requests by a collection of amp ids from database + * and returns {@link Future}<{@link StoredDataResult}>. + */ + @Override + public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(selectAmpStoredRequestsQuery, accountId, requestIds, Collections.emptySet(), timeout); + } + + /** + * Runs a process to get stored requests by a collection of video ids from database + * and returns {@link Future}<{@link StoredDataResult}>. + */ + @Override + public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); + } + + /** + * Runs a process to get stored responses by a collection of ids from database + * and returns {@link Future}<{@link StoredResponseDataResult}>. + */ + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + final String queryResolvedWithParameters = parametrizedQueryHelper.replaceStoredResponseIdPlaceholders( + selectStoredResponsesQuery, + responseIds.size()); + + final List idsQueryParameters = new ArrayList<>(); + final int responseIdPlaceholderCount = StringUtils.countMatches( + selectStoredResponsesQuery, + ParametrizedQueryHelper.RESPONSE_ID_PLACEHOLDER); + IntStream.rangeClosed(1, responseIdPlaceholderCount) + .forEach(i -> idsQueryParameters.addAll(responseIds)); + + return databaseClient.executeQuery(queryResolvedWithParameters, idsQueryParameters, + result -> DatabaseStoredResponseResultMapper.map(result, responseIds), timeout); + } + + /** + * Fetches stored requests from database for the given query. + */ + private Future fetchStoredData(String query, String accountId, Set requestIds, + Set impIds, Timeout timeout) { + final Future future; + + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + future = Future.succeededFuture( + StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList())); + } else { + final List idsQueryParameters = new ArrayList<>(); + IntStream.rangeClosed(1, StringUtils.countMatches(query, ParametrizedQueryHelper.REQUEST_ID_PLACEHOLDER)) + .forEach(i -> idsQueryParameters.addAll(requestIds)); + IntStream.rangeClosed(1, StringUtils.countMatches(query, ParametrizedQueryHelper.IMP_ID_PLACEHOLDER)) + .forEach(i -> idsQueryParameters.addAll(impIds)); + + final String parametrizedQuery = parametrizedQueryHelper.replaceRequestAndImpIdPlaceholders( + query, + requestIds.size(), + impIds.size()); + + future = databaseClient.executeQuery(parametrizedQuery, idsQueryParameters, + result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), + timeout); + } + + return future; + } +} diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 403583ad70f..11a0d2cb3af 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -1,56 +1,79 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.activity.utils.AccountActivitiesConfigurationUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; +import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.floors.PriceFloorsConfigResolver; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; -import org.prebid.server.log.ConditionalLogger; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountPriceFloorsConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class EnrichingApplicationSettings implements ApplicationSettings { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(EnrichingApplicationSettings.class)); - private final boolean enforceValidAccount; - private final double logSamplingRate; private final ApplicationSettings delegate; private final PriceFloorsConfigResolver priceFloorsConfigResolver; + private final ActivitiesConfigResolver activitiesConfigResolver; private final JsonMerger jsonMerger; - private final Account defaultAccount; public EnrichingApplicationSettings(boolean enforceValidAccount, - double logSamplingRate, - Account defaultAccount, + String defaultAccountConfig, ApplicationSettings delegate, PriceFloorsConfigResolver priceFloorsConfigResolver, - JsonMerger jsonMerger) { + ActivitiesConfigResolver activitiesConfigResolver, + JsonMerger jsonMerger, + JacksonMapper mapper) { this.enforceValidAccount = enforceValidAccount; - this.logSamplingRate = logSamplingRate; + this.activitiesConfigResolver = Objects.requireNonNull(activitiesConfigResolver); + this.priceFloorsConfigResolver = Objects.requireNonNull(priceFloorsConfigResolver); this.delegate = Objects.requireNonNull(delegate); this.jsonMerger = Objects.requireNonNull(jsonMerger); - this.priceFloorsConfigResolver = Objects.requireNonNull(priceFloorsConfigResolver); - this.defaultAccount = Objects.requireNonNull(defaultAccount); + + this.defaultAccount = parseAccount(defaultAccountConfig, mapper); } @Override public Future getAccountById(String accountId, Timeout timeout) { - return delegate.getAccountById(accountId, timeout) - .compose(priceFloorsConfigResolver::updateFloorsConfig) - .map(this::mergeAccounts) - .map(this::validateAndModifyAccount) - .recover(throwable -> recoverIfNeeded(throwable, accountId)); + if (StringUtils.isNotBlank(accountId)) { + return delegate.getAccountById(accountId, timeout) + .map(this::mergeAccounts) + .map(account -> priceFloorsConfigResolver.resolve(account, extractDefaultPriceFloors())) + .map(activitiesConfigResolver::resolve) + .recover(throwable -> recoverIfNeeded(throwable, accountId)); + } + + return recoverIfNeeded(new PreBidException("Unauthorized account: account id is empty"), StringUtils.EMPTY); + } + + private AccountPriceFloorsConfig extractDefaultPriceFloors() { + return Optional.ofNullable(defaultAccount) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceFloors) + .orElse(null); + } + + private static Account parseAccount(String accountConfig, JacksonMapper mapper) { + try { + return StringUtils.isNotBlank(accountConfig) + ? mapper.decodeValue(accountConfig, Account.class) + : null; + } catch (DecodeException e) { + throw new IllegalArgumentException("Could not parse default account configuration", e); + } } @Override @@ -91,32 +114,15 @@ public Future getVideoStoredData(String accountId, } private Account mergeAccounts(Account account) { - return jsonMerger.merge(account, defaultAccount, Account.class); - } - - private Account validateAndModifyAccount(Account account) { - if (AccountActivitiesConfigurationUtils.isInvalidActivitiesConfiguration(account)) { - conditionalLogger.warn( - "Activity configuration for account %s contains conditional rule with empty array." - .formatted(account.getId()), - logSamplingRate); - - final AccountPrivacyConfig accountPrivacyConfig = account.getPrivacy(); - return account.toBuilder() - .privacy(accountPrivacyConfig.toBuilder() - .activities(AccountActivitiesConfigurationUtils - .removeInvalidRules(accountPrivacyConfig.getActivities())) - .build()) - .build(); - } - - return account; + return defaultAccount == null + ? account + : jsonMerger.merge(account, defaultAccount, Account.class); } private Future recoverIfNeeded(Throwable throwable, String accountId) { // In case of invalid account return failed future - return !enforceValidAccount - ? Future.succeededFuture(mergeAccounts(Account.empty(accountId))) - : Future.failedFuture(throwable); + return enforceValidAccount + ? Future.failedFuture(throwable) + : Future.succeededFuture(mergeAccounts(Account.empty(accountId))); } } diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 262cceda164..1c78e4693c5 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -14,6 +12,8 @@ import org.prebid.server.execution.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.Category; import org.prebid.server.settings.model.StoredDataResult; @@ -22,8 +22,8 @@ import org.prebid.server.settings.proto.response.HttpAccountsResponse; import org.prebid.server.settings.proto.response.HttpFetcherResponse; import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java deleted file mode 100644 index a922c3af898..00000000000 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.prebid.server.settings; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonArray; -import io.vertx.ext.sql.ResultSet; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.settings.helper.JdbcStoredDataResultMapper; -import org.prebid.server.settings.helper.JdbcStoredResponseResultMapper; -import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.settings.model.StoredResponseDataResult; -import org.prebid.server.vertx.jdbc.JdbcClient; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -/** - * Implementation of {@link ApplicationSettings}. - *

- * Reads an application settings from the database source. - *

- * In order to enable caching and reduce latency for read operations {@link JdbcApplicationSettings} - * can be decorated by {@link CachingApplicationSettings}. - */ -public class JdbcApplicationSettings implements ApplicationSettings { - - private static final String ACCOUNT_ID_PLACEHOLDER = "%ACCOUNT_ID%"; - private static final String REQUEST_ID_PLACEHOLDER = "%REQUEST_ID_LIST%"; - private static final String IMP_ID_PLACEHOLDER = "%IMP_ID_LIST%"; - private static final String RESPONSE_ID_PLACEHOLDER = "%RESPONSE_ID_LIST%"; - private static final String QUERY_PARAM_PLACEHOLDER = "?"; - - private final JdbcClient jdbcClient; - private final JacksonMapper mapper; - - /** - * Query to select account by ids. - */ - private final String selectAccountQuery; - - /** - * Query to select stored requests and imps by ids, for example: - *

-     * SELECT accountId, reqid, requestData, 'request' as dataType
-     *   FROM stored_requests
-     *   WHERE reqid in (%REQUEST_ID_LIST%)
-     * UNION ALL
-     * SELECT accountId, impid, impData, 'imp' as dataType
-     *   FROM stored_imps
-     *   WHERE impid in (%IMP_ID_LIST%)
-     * 
- */ - private final String selectStoredRequestsQuery; - - /** - * Query to select amp stored requests by ids, for example: - *
-     * SELECT accountId, reqid, requestData, 'request' as dataType
-     *   FROM stored_requests
-     *   WHERE reqid in (%REQUEST_ID_LIST%)
-     * 
- */ - private final String selectAmpStoredRequestsQuery; - - /** - * Query to select stored responses by ids, for example: - *
-     * SELECT respid, responseData
-     *   FROM stored_responses
-     *   WHERE respid in (%RESPONSE_ID_LIST%)
-     * 
- */ - private final String selectStoredResponsesQuery; - - public JdbcApplicationSettings(JdbcClient jdbcClient, - JacksonMapper mapper, - String selectAccountQuery, - String selectStoredRequestsQuery, - String selectAmpStoredRequestsQuery, - String selectStoredResponsesQuery) { - - this.jdbcClient = Objects.requireNonNull(jdbcClient); - this.mapper = Objects.requireNonNull(mapper); - this.selectAccountQuery = Objects.requireNonNull(selectAccountQuery) - .replace(ACCOUNT_ID_PLACEHOLDER, QUERY_PARAM_PLACEHOLDER); - this.selectStoredRequestsQuery = Objects.requireNonNull(selectStoredRequestsQuery); - this.selectAmpStoredRequestsQuery = Objects.requireNonNull(selectAmpStoredRequestsQuery); - this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery); - } - - /** - * Runs a process to get account by id from database - * and returns {@link Future}<{@link Account}>. - */ - @Override - public Future getAccountById(String accountId, Timeout timeout) { - return jdbcClient.executeQuery( - selectAccountQuery, - Collections.singletonList(accountId), - result -> mapToModelOrError(result, row -> toAccount(row.getString(0))), - timeout) - .compose(result -> failedIfNull(result, accountId, "Account")); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return Future.failedFuture(new PreBidException("Not supported")); - } - - /** - * Transforms the first row of {@link ResultSet} to required object or returns null. - *

- * Note: mapper should never throws exception in case of using - * {@link org.prebid.server.vertx.jdbc.CircuitBreakerSecuredJdbcClient}. - */ - private T mapToModelOrError(ResultSet result, Function mapper) { - return result != null && CollectionUtils.isNotEmpty(result.getResults()) - ? mapper.apply(result.getResults().get(0)) - : null; - } - - /** - * Returns succeeded {@link Future} if given value is not equal to NULL, - * otherwise failed {@link Future} with {@link PreBidException}. - */ - private static Future failedIfNull(T value, String id, String errorPrefix) { - return value != null - ? Future.succeededFuture(value) - : Future.failedFuture(new PreBidException("%s not found: %s".formatted(errorPrefix, id))); - } - - private Account toAccount(String source) { - try { - return source != null ? mapper.decodeValue(source, Account.class) : null; - } catch (DecodeException e) { - throw new PreBidException(e.getMessage()); - } - } - - /** - * Runs a process to get stored requests by a collection of ids from database - * and returns {@link Future}<{@link StoredDataResult}>. - */ - @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); - } - - /** - * Runs a process to get stored requests by a collection of amp ids from database - * and returns {@link Future}<{@link StoredDataResult}>. - */ - @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return fetchStoredData(selectAmpStoredRequestsQuery, accountId, requestIds, Collections.emptySet(), timeout); - } - - /** - * Runs a process to get stored requests by a collection of video ids from database - * and returns {@link Future}<{@link StoredDataResult}>. - */ - @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); - } - - /** - * Runs a process to get stored responses by a collection of ids from database - * and returns {@link Future}<{@link StoredResponseDataResult}>. - */ - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - final String queryResolvedWithParameters = selectStoredResponsesQuery.replaceAll(RESPONSE_ID_PLACEHOLDER, - parameterHolders(responseIds.size())); - - final List idsQueryParameters = new ArrayList<>(); - IntStream.rangeClosed(1, StringUtils.countMatches(selectStoredResponsesQuery, RESPONSE_ID_PLACEHOLDER)) - .forEach(i -> idsQueryParameters.addAll(responseIds)); - - return jdbcClient.executeQuery(queryResolvedWithParameters, idsQueryParameters, - result -> JdbcStoredResponseResultMapper.map(result, responseIds), timeout); - } - - /** - * Fetches stored requests from database for the given query. - */ - private Future fetchStoredData(String query, String accountId, Set requestIds, - Set impIds, Timeout timeout) { - final Future future; - - if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { - future = Future.succeededFuture( - StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList())); - } else { - final List idsQueryParameters = new ArrayList<>(); - IntStream.rangeClosed(1, StringUtils.countMatches(query, REQUEST_ID_PLACEHOLDER)) - .forEach(i -> idsQueryParameters.addAll(requestIds)); - IntStream.rangeClosed(1, StringUtils.countMatches(query, IMP_ID_PLACEHOLDER)) - .forEach(i -> idsQueryParameters.addAll(impIds)); - - final String parametrizedQuery = createParametrizedQuery(query, requestIds.size(), impIds.size()); - future = jdbcClient.executeQuery(parametrizedQuery, idsQueryParameters, - result -> JdbcStoredDataResultMapper.map(result, accountId, requestIds, impIds), - timeout); - } - - return future; - } - - /** - * Creates parametrized query from query and variable templates, by replacing templateVariable - * with appropriate number of "?" placeholders. - */ - private static String createParametrizedQuery(String query, int requestIdsSize, int impIdsSize) { - return query - .replace(REQUEST_ID_PLACEHOLDER, parameterHolders(requestIdsSize)) - .replace(IMP_ID_PLACEHOLDER, parameterHolders(impIdsSize)); - } - - /** - * Returns string for parametrized placeholder. - */ - private static String parameterHolders(int paramsSize) { - return paramsSize == 0 - ? "NULL" - : IntStream.range(0, paramsSize) - .mapToObj(i -> QUERY_PARAM_PLACEHOLDER) - .collect(Collectors.joining(",")); - } -} diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java new file mode 100644 index 00000000000..f6198a5ad94 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -0,0 +1,227 @@ +package org.prebid.server.settings; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; +import software.amazon.awssdk.core.BytesWrapper; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Implementation of {@link ApplicationSettings}. + *

+ * Reads an application settings from JSON file in a s3 bucket, stores and serves them in and from the memory. + *

+ * Immediately loads stored request data from local files. These are stored in memory for low-latency reads. + * This expects each file in the directory to be named "{config_id}.json". + */ +public class S3ApplicationSettings implements ApplicationSettings { + + private static final String JSON_SUFFIX = ".json"; + + final S3AsyncClient asyncClient; + final String bucket; + final String accountsDirectory; + final String storedImpressionsDirectory; + final String storedRequestsDirectory; + final String storedResponsesDirectory; + final JacksonMapper jacksonMapper; + final Vertx vertx; + + public S3ApplicationSettings(S3AsyncClient asyncClient, + String bucket, + String accountsDirectory, + String storedImpressionsDirectory, + String storedRequestsDirectory, + String storedResponsesDirectory, + JacksonMapper jacksonMapper, + Vertx vertx) { + + this.asyncClient = Objects.requireNonNull(asyncClient); + this.bucket = Objects.requireNonNull(bucket); + this.accountsDirectory = Objects.requireNonNull(accountsDirectory); + this.storedImpressionsDirectory = Objects.requireNonNull(storedImpressionsDirectory); + this.storedRequestsDirectory = Objects.requireNonNull(storedRequestsDirectory); + this.storedResponsesDirectory = Objects.requireNonNull(storedResponsesDirectory); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.vertx = Objects.requireNonNull(vertx); + } + + @Override + public Future getAccountById(String accountId, Timeout timeout) { + return withTimeout(() -> downloadFile(accountsDirectory + "/" + accountId + JSON_SUFFIX), timeout) + .map(fileContent -> decodeAccount(fileContent, accountId)); + } + + private Account decodeAccount(String fileContent, String requestedAccountId) { + if (fileContent == null) { + throw new PreBidException("Account with id %s not found".formatted(requestedAccountId)); + } + + final Account account; + try { + account = jacksonMapper.decodeValue(fileContent, Account.class); + } catch (DecodeException e) { + throw new PreBidException("Invalid json for account with id %s".formatted(requestedAccountId)); + } + + validateAccount(account, requestedAccountId); + return account; + } + + private static void validateAccount(Account account, String requestedAccountId) { + final String receivedAccountId = account != null ? account.getId() : null; + if (!StringUtils.equals(receivedAccountId, requestedAccountId)) { + throw new PreBidException( + "Account with id %s does not match id %s in file".formatted(requestedAccountId, receivedAccountId)); + } + } + + @Override + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return withTimeout( + () -> Future.all( + getFileContents(storedRequestsDirectory, requestIds), + getFileContents(storedImpressionsDirectory, impIds)), + timeout) + .map(results -> buildStoredDataResult( + results.resultAt(0), + results.resultAt(1), + requestIds, + impIds)); + } + + private StoredDataResult buildStoredDataResult(Map storedIdToRequest, + Map storedIdToImp, + Set requestIds, + Set impIds) { + + final List errors = Stream.concat( + missingStoredDataIds(storedIdToImp, impIds).stream() + .map("No stored impression found for id: %s"::formatted), + missingStoredDataIds(storedIdToRequest, requestIds).stream() + .map("No stored request found for id: %s"::formatted)) + .toList(); + + return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); + } + + private Set missingStoredDataIds(Map fileContents, Set responseIds) { + return SetUtils.difference(responseIds, fileContents.keySet()); + } + + @Override + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + } + + @Override + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return withTimeout(() -> getFileContents(storedResponsesDirectory, responseIds), timeout) + .map(storedIdToResponse -> StoredResponseDataResult.of( + storedIdToResponse, + missingStoredDataIds(storedIdToResponse, responseIds).stream() + .map("No stored response found for id: %s"::formatted) + .toList())); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return Future.succeededFuture(Collections.emptyMap()); + } + + private Future> getFileContents(String directory, Set ids) { + return Future.join(ids.stream() + .map(impId -> downloadFile(directory + withInitialSlash(impId) + JSON_SUFFIX) + .map(fileContent -> Tuple2.of(impId, fileContent))) + .toList()) + .map(CompositeFuture::>list) + .map(impIdToFileContent -> impIdToFileContent.stream() + .filter(tuple -> tuple.getRight() != null) + .collect(Collectors.toMap(Tuple2::getLeft, Tuple2::getRight))); + } + + /** + * When the impression id is the ad unit path it may already start with a slash and there's no need to add + * another one. + * + * @param impressionId from the bid request + * @return impression id with only a single slash at the beginning + */ + private static String withInitialSlash(String impressionId) { + return impressionId.startsWith("/") ? impressionId : "/" + impressionId; + } + + private Future downloadFile(String key) { + final GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + return Future.fromCompletionStage( + asyncClient.getObject(request, AsyncResponseTransformer.toBytes()), + vertx.getOrCreateContext()) + .map(BytesWrapper::asUtf8String) + .otherwiseEmpty(); + } + + private Future withTimeout(Supplier> futureFactory, Timeout timeout) { + final long remainingTime = timeout.remaining(); + if (remainingTime <= 0L) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final Promise promise = Promise.promise(); + final Future future = futureFactory.get(); + + final long timerId = vertx.setTimer(remainingTime, id -> + promise.tryFail(new TimeoutException("Timeout has been exceeded"))); + + future.onComplete(result -> { + vertx.cancelTimer(timerId); + if (result.succeeded()) { + promise.tryComplete(result.result()); + } else { + promise.tryFail(result.cause()); + } + }); + + return promise.future(); + } +} diff --git a/src/main/java/org/prebid/server/settings/SettingsCache.java b/src/main/java/org/prebid/server/settings/SettingsCache.java index 1bba204db36..3e2446742b1 100644 --- a/src/main/java/org/prebid/server/settings/SettingsCache.java +++ b/src/main/java/org/prebid/server/settings/SettingsCache.java @@ -1,8 +1,10 @@ package org.prebid.server.settings; import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; +import org.checkerframework.checker.index.qual.NonNegative; import org.prebid.server.settings.model.StoredItem; import java.util.Collections; @@ -10,7 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadLocalRandom; /** * Just a simple wrapper over in-memory caches for requests and imps. @@ -20,17 +22,26 @@ public class SettingsCache implements CacheNotificationListener { private final Map> requestCache; private final Map> impCache; - public SettingsCache(int ttl, int size) { + public SettingsCache(int ttl, int size, int jitter) { if (ttl <= 0 || size <= 0) { throw new IllegalArgumentException("ttl and size must be positive"); } - requestCache = createCache(ttl, size); - impCache = createCache(ttl, size); + if (jitter < 0 || jitter >= ttl) { + throw new IllegalArgumentException("jitter must match the inequality: 0 <= jitter < ttl"); + } + + requestCache = createCache(ttl, size, jitter); + impCache = createCache(ttl, size, jitter); } - public static Map createCache(int ttl, int size) { + public static Map createCache(int ttlSeconds, int size, int jitterSeconds) { + final long expireAfterNanos = (long) (ttlSeconds * 1e9); + final long jitterNanos = jitterSeconds == 0 ? 0L : (long) (jitterSeconds * 1e9); + return Caffeine.newBuilder() - .expireAfterWrite(ttl, TimeUnit.SECONDS) + .expireAfter(jitterNanos == 0L + ? new StaticExpiry<>(expireAfterNanos) + : new ExpiryWithJitter<>(expireAfterNanos, jitterNanos)) .maximumSize(size) .build() .asMap(); @@ -53,7 +64,10 @@ void saveImpCache(String accountId, String impId, String impValue) { } private static void saveCachedValue(Map> cache, - String accountId, String id, String value) { + String accountId, + String id, + String value) { + final Set values = ObjectUtils.defaultIfNull(cache.get(id), new HashSet<>()); values.add(StoredItem.of(accountId, value)); cache.put(id, values); @@ -79,4 +93,58 @@ public void invalidate(List requests, List imps) { requests.forEach(requestCache.keySet()::remove); imps.forEach(impCache.keySet()::remove); } + + private static class StaticExpiry implements Expiry { + + private final long expireAfterNanos; + + private StaticExpiry(long expireAfterNanos) { + this.expireAfterNanos = expireAfterNanos; + } + + @Override + public long expireAfterCreate(K key, V value, long currentTime) { + return expireAfterNanos; + } + + @Override + public long expireAfterUpdate(K key, V value, long currentTime, @NonNegative long currentDuration) { + return expireAfterNanos; + } + + @Override + public long expireAfterRead(K key, V value, long currentTime, @NonNegative long currentDuration) { + return currentDuration; + } + } + + private static class ExpiryWithJitter implements Expiry { + + private final Expiry baseExpiry; + private final long jitterNanos; + + private ExpiryWithJitter(long baseExpireAfterNanos, long jitterNanos) { + this.baseExpiry = new StaticExpiry<>(baseExpireAfterNanos); + this.jitterNanos = jitterNanos; + } + + @Override + public long expireAfterCreate(K key, V value, long currentTime) { + return baseExpiry.expireAfterCreate(key, value, currentTime) + jitter(); + } + + @Override + public long expireAfterUpdate(K key, V value, long currentTime, @NonNegative long currentDuration) { + return baseExpiry.expireAfterUpdate(key, value, currentTime, currentDuration) + jitter(); + } + + @Override + public long expireAfterRead(K key, V value, long currentTime, @NonNegative long currentDuration) { + return baseExpiry.expireAfterRead(key, value, currentTime, currentDuration); + } + + private long jitter() { + return ThreadLocalRandom.current().nextLong(-jitterNanos, jitterNanos); + } + } } diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java new file mode 100644 index 00000000000..00853194e65 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java @@ -0,0 +1,172 @@ +package org.prebid.server.settings.helper; + +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredItem; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility class for mapping {@link RowSet} to {@link StoredDataResult}. + */ +public class DatabaseStoredDataResultMapper { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseStoredDataResultMapper.class); + + private DatabaseStoredDataResultMapper() { + } + + /** + * Maps {@link RowSet} to {@link StoredDataResult} and creates an error for each missing ID and add it to result. + * + * @param rowSet - incoming Row Set representing a result of SQL query + * @param accountId - an account ID extracted from request + * @param requestIds - a specified set of stored requests' IDs. Adds error for each ID missing in result set + * @param impIds - a specified set of stored imps' IDs. Adds error for each ID missing in result set + * @return - a {@link StoredDataResult} object + *

+ * Note: mapper should never throws exception in case of using + * {@link CircuitBreakerSecuredDatabaseClient}. + */ + public static StoredDataResult map(RowSet rowSet, + String accountId, + Set requestIds, + Set impIds) { + final Map storedIdToRequest; + final Map storedIdToImp; + final List errors = new ArrayList<>(); + + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + + if (rowIterator == null || !rowIterator.hasNext()) { + storedIdToRequest = Collections.emptyMap(); + storedIdToImp = Collections.emptyMap(); + + if (requestIds.isEmpty() && impIds.isEmpty()) { + errors.add("No stored requests or imps were found"); + } else { + final String errorRequests = requestIds.isEmpty() ? "" + : "stored requests for ids " + requestIds; + final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; + final String errorImps = impIds.isEmpty() ? "" : "stored imps for ids " + impIds; + + errors.add("No %s%s%s were found".formatted(errorRequests, separator, errorImps)); + } + } else { + final Map> requestIdToStoredItems = new HashMap<>(); + final Map> impIdToStoredItems = new HashMap<>(); + + while (rowIterator.hasNext()) { + final Row row = rowIterator.next(); + if (row.toJson().size() < 4) { + final String message = "Error occurred while mapping stored request data: some columns are missing"; + logger.error(message); + errors.add(message); + return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); + } + final String fetchedAccountId; + final String id; + final String data; + final String typeAsString; + try { + fetchedAccountId = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); + id = ObjectUtil.getIfNotNull(row.getValue(1), Object::toString); + data = ObjectUtil.getIfNotNull(row.getValue(2), Object::toString); + typeAsString = ObjectUtil.getIfNotNull(row.getValue(3), Object::toString); + } catch (ClassCastException e) { + final String message = "Error occurred while mapping stored request data"; + logger.error(message, e); + errors.add(message); + return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); + } + + final StoredDataType type; + try { + type = StoredDataType.valueOf(typeAsString); + } catch (IllegalArgumentException e) { + logger.error("Stored request data with id={} has invalid type: ''{}'' and will be ignored.", e, + id, typeAsString); + continue; + } + + if (type == StoredDataType.request) { + addStoredItem(fetchedAccountId, id, data, requestIdToStoredItems); + } else { + addStoredItem(fetchedAccountId, id, data, impIdToStoredItems); + } + } + + storedIdToRequest = storedItemsOrAddError(StoredDataType.request, accountId, requestIds, + requestIdToStoredItems, errors); + storedIdToImp = storedItemsOrAddError(StoredDataType.imp, accountId, impIds, + impIdToStoredItems, errors); + } + + return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); + } + + /** + * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. + * + * @param resultSet - incoming {@link RowSet} representing a result of SQL query. + * @return - a {@link StoredDataResult} object. + */ + public static StoredDataResult map(RowSet resultSet) { + return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); + } + + private static void addStoredItem(String accountId, String id, String data, + Map> idToStoredItems) { + final StoredItem storedItem = StoredItem.of(accountId, data); + + final Set storedItems = idToStoredItems.get(id); + if (storedItems == null) { + idToStoredItems.put(id, new HashSet<>(Collections.singleton(storedItem))); + } else { + storedItems.add(storedItem); + } + } + + /** + * Returns map of stored ID -> value or populates error. + */ + private static Map storedItemsOrAddError(StoredDataType type, + String accountId, + Set searchIds, + Map> foundIdToStoredItems, + List errors) { + final Map result = new HashMap<>(); + + if (searchIds.isEmpty()) { + for (Map.Entry> entry : foundIdToStoredItems.entrySet()) { + entry.getValue().forEach(storedItem -> result.put(entry.getKey(), storedItem.getData())); + } + } else { + for (String id : searchIds) { + try { + final StoredItem resolvedStoredItem = StoredItemResolver.resolve(type, accountId, id, + foundIdToStoredItems.get(id)); + result.put(id, resolvedStoredItem.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); + } + } + } + + return result; + } +} diff --git a/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java new file mode 100644 index 00000000000..99896d72911 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java @@ -0,0 +1,56 @@ +package org.prebid.server.settings.helper; + +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DatabaseStoredResponseResultMapper { + + private DatabaseStoredResponseResultMapper() { + } + + public static StoredResponseDataResult map(RowSet rowSet, Set responseIds) { + final Map storedIdToResponse = new HashMap<>(responseIds.size()); + final List errors = new ArrayList<>(); + + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + if (rowIterator == null || !rowIterator.hasNext()) { + handleEmptyResultError(responseIds, errors); + return StoredResponseDataResult.of(storedIdToResponse, errors); + } + + while (rowIterator.hasNext()) { + final Row row = rowIterator.next(); + if (row.toJson().size() < 2) { + errors.add("Result set column number is less than expected"); + return StoredResponseDataResult.of(Collections.emptyMap(), errors); + } + final String key = ObjectUtil.getIfNotNull(row.getValue(0), Object::toString); + final String value = ObjectUtil.getIfNotNull(row.getValue(1), Object::toString); + storedIdToResponse.put(key, value); + } + + errors.addAll(responseIds.stream().filter(id -> !storedIdToResponse.containsKey(id)) + .map(id -> "No stored response found for id: " + id) + .toList()); + + return StoredResponseDataResult.of(storedIdToResponse, errors); + } + + private static void handleEmptyResultError(Set responseIds, List errors) { + if (responseIds.isEmpty()) { + errors.add("No stored responses found"); + } else { + errors.add("No stored responses were found for ids: " + String.join(",", responseIds)); + } + } +} diff --git a/src/main/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapper.java deleted file mode 100644 index 3cb710193e2..00000000000 --- a/src/main/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapper.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.prebid.server.settings.helper; - -import io.vertx.core.json.JsonArray; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.sql.ResultSet; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.settings.model.StoredDataType; -import org.prebid.server.settings.model.StoredItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Utility class for mapping {@link ResultSet} to {@link StoredDataResult}. - */ -public class JdbcStoredDataResultMapper { - - private static final Logger logger = LoggerFactory.getLogger(JdbcStoredDataResultMapper.class); - - private JdbcStoredDataResultMapper() { - } - - /** - * Maps {@link ResultSet} to {@link StoredDataResult} and creates an error for each missing ID and add it to result. - * - * @param resultSet - incoming Result Set representing a result of SQL query - * @param accountId - an account ID extracted from request - * @param requestIds - a specified set of stored requests' IDs. Adds error for each ID missing in result set - * @param impIds - a specified set of stored imps' IDs. Adds error for each ID missing in result set - * @return - a {@link StoredDataResult} object - *

- * Note: mapper should never throws exception in case of using - * {@link org.prebid.server.vertx.jdbc.CircuitBreakerSecuredJdbcClient}. - */ - public static StoredDataResult map(ResultSet resultSet, String accountId, Set requestIds, - Set impIds) { - final Map storedIdToRequest; - final Map storedIdToImp; - final List errors = new ArrayList<>(); - - if (resultSet == null || CollectionUtils.isEmpty(resultSet.getResults())) { - storedIdToRequest = Collections.emptyMap(); - storedIdToImp = Collections.emptyMap(); - - if (requestIds.isEmpty() && impIds.isEmpty()) { - errors.add("No stored requests or imps were found"); - } else { - final String errorRequests = requestIds.isEmpty() ? "" - : "stored requests for ids " + requestIds; - final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; - final String errorImps = impIds.isEmpty() ? "" : "stored imps for ids " + impIds; - - errors.add("No %s%s%s were found".formatted(errorRequests, separator, errorImps)); - } - } else { - final Map> requestIdToStoredItems = new HashMap<>(); - final Map> impIdToStoredItems = new HashMap<>(); - - for (JsonArray result : resultSet.getResults()) { - final String fetchedAccountId; - final String id; - final String data; - final String typeAsString; - try { - fetchedAccountId = result.getString(0); - id = result.getString(1); - data = result.getString(2); - typeAsString = result.getString(3); - } catch (IndexOutOfBoundsException | ClassCastException e) { - final String message = "Error occurred while mapping stored request data"; - logger.error(message, e); - errors.add(message); - return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); - } - - final StoredDataType type; - try { - type = StoredDataType.valueOf(typeAsString); - } catch (IllegalArgumentException e) { - logger.error("Stored request data with id={0} has invalid type: ''{1}'' and will be ignored.", e, - id, typeAsString); - continue; - } - - if (type == StoredDataType.request) { - addStoredItem(fetchedAccountId, id, data, requestIdToStoredItems); - } else { - addStoredItem(fetchedAccountId, id, data, impIdToStoredItems); - } - } - - storedIdToRequest = storedItemsOrAddError(StoredDataType.request, accountId, requestIds, - requestIdToStoredItems, errors); - storedIdToImp = storedItemsOrAddError(StoredDataType.imp, accountId, impIds, - impIdToStoredItems, errors); - } - - return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); - } - - /** - * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. - * - * @param resultSet - incoming {@link ResultSet} representing a result of SQL query. - * @return - a {@link StoredDataResult} object. - */ - public static StoredDataResult map(ResultSet resultSet) { - return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); - } - - private static void addStoredItem(String accountId, String id, String data, - Map> idToStoredItems) { - final StoredItem storedItem = StoredItem.of(accountId, data); - - final Set storedItems = idToStoredItems.get(id); - if (storedItems == null) { - idToStoredItems.put(id, new HashSet<>(Collections.singleton(storedItem))); - } else { - storedItems.add(storedItem); - } - } - - /** - * Returns map of stored ID -> value or populates error. - */ - private static Map storedItemsOrAddError(StoredDataType type, - String accountId, - Set searchIds, - Map> foundIdToStoredItems, - List errors) { - final Map result = new HashMap<>(); - - if (searchIds.isEmpty()) { - for (Map.Entry> entry : foundIdToStoredItems.entrySet()) { - entry.getValue().forEach(storedItem -> result.put(entry.getKey(), storedItem.getData())); - } - } else { - for (String id : searchIds) { - try { - final StoredItem resolvedStoredItem = StoredItemResolver.resolve(type, accountId, id, - foundIdToStoredItems.get(id)); - result.put(id, resolvedStoredItem.getData()); - } catch (PreBidException e) { - errors.add(e.getMessage()); - } - } - } - - return result; - } -} diff --git a/src/main/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapper.java b/src/main/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapper.java deleted file mode 100644 index 4d6b5208cbd..00000000000 --- a/src/main/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapper.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.prebid.server.settings.helper; - -import io.vertx.core.json.JsonArray; -import io.vertx.ext.sql.ResultSet; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.settings.model.StoredResponseDataResult; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class JdbcStoredResponseResultMapper { - - private JdbcStoredResponseResultMapper() { - } - - public static StoredResponseDataResult map(ResultSet resultSet, Set responseIds) { - final Map storedIdToResponse = new HashMap<>(responseIds.size()); - final List errors = new ArrayList<>(); - - if (resultSet == null || CollectionUtils.isEmpty(resultSet.getResults())) { - handleEmptyResultError(responseIds, errors); - } else { - try { - for (JsonArray result : resultSet.getResults()) { - storedIdToResponse.put(result.getString(0), result.getString(1)); - } - } catch (IndexOutOfBoundsException e) { - errors.add("Result set column number is less than expected"); - return StoredResponseDataResult.of(Collections.emptyMap(), errors); - } - errors.addAll(responseIds.stream().filter(id -> !storedIdToResponse.containsKey(id)) - .map(id -> "No stored response found for id: " + id) - .toList()); - } - - return StoredResponseDataResult.of(storedIdToResponse, errors); - } - - private static void handleEmptyResultError(Set responseIds, List errors) { - if (responseIds.isEmpty()) { - errors.add("No stored responses found"); - } else { - errors.add("No stored responses were found for ids: " + String.join(",", responseIds)); - } - } -} diff --git a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java new file mode 100644 index 00000000000..1d7660e0b76 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.helper; + +public interface ParametrizedQueryHelper { + + String ACCOUNT_ID_PLACEHOLDER = "%ACCOUNT_ID%"; + String REQUEST_ID_PLACEHOLDER = "%REQUEST_ID_LIST%"; + String IMP_ID_PLACEHOLDER = "%IMP_ID_LIST%"; + String RESPONSE_ID_PLACEHOLDER = "%RESPONSE_ID_LIST%"; + + String replaceAccountIdPlaceholder(String query); + + String replaceStoredResponseIdPlaceholders(String query, int idsNumber); + + String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber); + +} diff --git a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java new file mode 100644 index 00000000000..e6e6336afc8 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryMySqlHelper.java @@ -0,0 +1,34 @@ +package org.prebid.server.settings.helper; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ParametrizedQueryMySqlHelper implements ParametrizedQueryHelper { + + private static final String PARAMETER_PLACEHOLDER = "?"; + + @Override + public String replaceAccountIdPlaceholder(String query) { + return query.replace(ACCOUNT_ID_PLACEHOLDER, PARAMETER_PLACEHOLDER); + } + + @Override + public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { + return query.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber)); + } + + @Override + public String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber) { + return query + .replace(REQUEST_ID_PLACEHOLDER, parameterHolders(requestIdNumber)) + .replace(IMP_ID_PLACEHOLDER, parameterHolders(impIdNumber)); + } + + private static String parameterHolders(int paramsSize) { + return paramsSize == 0 + ? "NULL" + : IntStream.range(0, paramsSize) + .mapToObj(i -> PARAMETER_PLACEHOLDER) + .collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java new file mode 100644 index 00000000000..c380b8e91df --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java @@ -0,0 +1,45 @@ +package org.prebid.server.settings.helper; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ParametrizedQueryPostgresHelper implements ParametrizedQueryHelper { + + private static final Pattern PLACEHOLDER_PATTERN = + Pattern.compile("(%s)|(%s)".formatted(REQUEST_ID_PLACEHOLDER, IMP_ID_PLACEHOLDER)); + + @Override + public String replaceAccountIdPlaceholder(String query) { + return query.replace(ACCOUNT_ID_PLACEHOLDER, "$1"); + } + + @Override + public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { + return query.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber, 0)); + } + + @Override + public String replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber) { + final Matcher matcher = PLACEHOLDER_PATTERN.matcher(query); + + int i = 0; + final StringBuilder queryBuilder = new StringBuilder(); + while (matcher.find()) { + final int paramsNumber = matcher.group(1) != null ? requestIdNumber : impIdNumber; + matcher.appendReplacement(queryBuilder, parameterHolders(paramsNumber, i)); + i += paramsNumber; + } + matcher.appendTail(queryBuilder); + return queryBuilder.toString(); + } + + private static String parameterHolders(int paramsSize, int start) { + return paramsSize == 0 + ? "NULL" + : IntStream.range(start, start + paramsSize) + .mapToObj(i -> "\\$" + (i + 1)) + .collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index b26969b87e0..c54998320d4 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -1,7 +1,6 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Builder; import lombok.Value; @@ -21,17 +20,14 @@ public class Account { AccountMetricsConfig metrics; - @JsonProperty("cookie-sync") + @JsonAlias("cookie-sync") AccountCookieSyncConfig cookieSync; AccountHooksConfiguration hooks; + AccountSettings settings; + public static Account empty(String id) { return Account.builder().id(id).build(); } - - @JsonIgnore - public boolean isEmpty() { - return this.equals(empty(id)); - } } diff --git a/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java index 3664daa2fe5..24f0b49d8ba 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java @@ -1,6 +1,6 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; @@ -18,7 +18,10 @@ public class AccountAnalyticsConfig { "app", true); } - @JsonProperty("auction-events") + @JsonAlias("allow-client-details") + boolean allowClientDetails; + + @JsonAlias("auction-events") AccountAuctionEventConfig auctionEvents; Map modules; diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index c1d85d76532..9708c23bb1c 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; @@ -11,30 +12,30 @@ @Value public class AccountAuctionConfig { - @JsonProperty("price-granularity") + @JsonAlias("price-granularity") String priceGranularity; - @JsonProperty("banner-cache-ttl") + @JsonAlias("banner-cache-ttl") Integer bannerCacheTtl; - @JsonProperty("video-cache-ttl") + @JsonAlias("video-cache-ttl") Integer videoCacheTtl; - @JsonProperty("truncate-target-attr") + @JsonAlias("truncate-target-attr") Integer truncateTargetAttr; - @JsonProperty("default-integration") + @JsonAlias("default-integration") String defaultIntegration; - @JsonProperty("debug-allow") + @JsonAlias("debug-allow") Boolean debugAllow; - @JsonProperty("bid-validations") + @JsonAlias("bid-validations") AccountBidValidationConfig bidValidations; AccountEventsConfig events; - @JsonProperty("price-floors") + @JsonAlias("price-floors") AccountPriceFloorsConfig priceFloors; AccountTargetingConfig targeting; diff --git a/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java b/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java index 38c5afb2bfb..a6cfa2d19a7 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java @@ -1,11 +1,13 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @Value(staticConstructor = "of") public class AccountBidValidationConfig { - @JsonProperty("banner-creative-max-size") + @JsonProperty("banner_creative_max_size") + @JsonAlias("banner-creative-max-size") BidValidationEnforcement bannerMaxSizeEnforcement; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java index 8f819788db6..64a22131808 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -12,9 +13,9 @@ @Data public class AccountCcpaConfig { - @JsonProperty("enabled") Boolean enabled; - @JsonProperty("channel-enabled") + @JsonProperty("channel_enabled") + @JsonAlias("channel-enabled") EnabledForRequestType enabledForRequestType; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java index 37691fa6776..755b81bd3b3 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @@ -8,15 +9,15 @@ @Value(staticConstructor = "of") public class AccountCookieSyncConfig { - @JsonProperty("default-limit") + @JsonAlias("default-limit") Integer defaultLimit; - @JsonProperty("max-limit") + @JsonAlias("max-limit") Integer maxLimit; @JsonProperty("pri") Set prioritizedBidders; - @JsonProperty("coop-sync") + @JsonAlias("coop-sync") AccountCoopSyncConfig coopSync; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java b/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java index 59d8913340e..c09e3d5a8d4 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountDsaConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @@ -9,6 +10,6 @@ public class AccountDsaConfig { @JsonProperty("default") DefaultDsa defaultDsa; - @JsonProperty("gdpr-only") + @JsonAlias("gdpr-only") Boolean gdprOnly; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java b/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java index 570284a4787..756ab6f7278 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; @@ -10,20 +11,23 @@ @Value public class AccountGdprConfig { - @JsonProperty("enabled") Boolean enabled; - @JsonProperty("channel-enabled") + @JsonAlias("eea-countries") + String eeaCountries; + + @JsonProperty("channel_enabled") + @JsonAlias("channel-enabled") EnabledForRequestType enabledForRequestType; Purposes purposes; - @JsonProperty("special-features") + @JsonAlias("special-features") SpecialFeatures specialFeatures; - @JsonProperty("purpose-one-treatment-interpretation") + @JsonAlias("purpose-one-treatment-interpretation") PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation; - @JsonProperty("basic-enforcement-vendors") + @JsonAlias("basic-enforcement-vendors") List basicEnforcementVendors; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java index 9d7788b084b..75d3b03b6e5 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java +++ b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java @@ -1,6 +1,6 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; import org.prebid.server.hooks.execution.model.ExecutionPlan; @@ -10,7 +10,7 @@ @Value(staticConstructor = "of") public class AccountHooksConfiguration { - @JsonProperty("execution-plan") + @JsonAlias("execution-plan") ExecutionPlan executionPlan; Map modules; diff --git a/src/main/java/org/prebid/server/settings/model/AccountMetricsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountMetricsConfig.java index 6daac1b3053..318198baa9a 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountMetricsConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountMetricsConfig.java @@ -1,14 +1,12 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Value; import org.prebid.server.metric.model.AccountMetricsVerbosityLevel; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class AccountMetricsConfig { - @JsonProperty("verbosity-level") + @JsonAlias("verbosity-level") AccountMetricsVerbosityLevel verbosityLevel; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java index b7153c07799..76be21ea70e 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java @@ -1,6 +1,6 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Builder; import lombok.Value; @@ -12,15 +12,15 @@ public class AccountPriceFloorsConfig { AccountPriceFloorsFetchConfig fetch; - @JsonProperty("enforce-floors-rate") + @JsonAlias("enforce-floors-rate") Integer enforceFloorsRate; - @JsonProperty("adjust-for-bid-adjustment") + @JsonAlias("adjust-for-bid-adjustment") Boolean adjustForBidAdjustment; - @JsonProperty("enforce-deal-floors") + @JsonAlias("enforce-deal-floors") Boolean enforceDealFloors; - @JsonProperty("use-dynamic-data") + @JsonAlias("use-dynamic-data") Boolean useDynamicData; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java index eb87f0e4230..2cb3854371a 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java @@ -1,6 +1,6 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Builder; import lombok.Value; @@ -12,18 +12,18 @@ public class AccountPriceFloorsFetchConfig { String url; - @JsonProperty("timeout-ms") - Long timeout; + @JsonAlias("timeout-ms") + Long timeoutMs; - @JsonProperty("max-file-size-kb") - Long maxFileSize; + @JsonAlias("max-file-size-kb") + Long maxFileSizeKb; - @JsonProperty("max-rules") + @JsonAlias("max-rules") Long maxRules; - @JsonProperty("max-age-sec") + @JsonAlias("max-age-sec") Long maxAgeSec; - @JsonProperty("period-sec") + @JsonAlias("period-sec") Long periodSec; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxConfig.java index 3ab2e0aefcb..b42cc7723f8 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxConfig.java @@ -8,5 +8,4 @@ public class AccountPrivacySandboxConfig { @JsonProperty("cookiedeprecation") AccountPrivacySandboxCookieDeprecationConfig cookieDeprecation; - } diff --git a/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxCookieDeprecationConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxCookieDeprecationConfig.java index da0b04a5520..075d5f55d31 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxCookieDeprecationConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountPrivacySandboxCookieDeprecationConfig.java @@ -10,5 +10,4 @@ public class AccountPrivacySandboxCookieDeprecationConfig { @JsonProperty("ttlsec") Long ttlSec; - } diff --git a/src/main/java/org/prebid/server/settings/model/AccountSettings.java b/src/main/java/org/prebid/server/settings/model/AccountSettings.java new file mode 100644 index 00000000000..144cdf428a6 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountSettings.java @@ -0,0 +1,11 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountSettings { + + @JsonAlias("geo-lookup") + Boolean geoLookup; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java b/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java index a47dfa0a614..8b0e6d346bc 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java @@ -23,6 +23,5 @@ public class AccountTargetingConfig { @JsonProperty("alwaysincludedeals") Boolean alwaysIncludeDeals; - @JsonProperty("prefix") String prefix; } diff --git a/src/main/java/org/prebid/server/settings/model/GdprConfig.java b/src/main/java/org/prebid/server/settings/model/GdprConfig.java index c1ddf431e71..f4daf274da7 100644 --- a/src/main/java/org/prebid/server/settings/model/GdprConfig.java +++ b/src/main/java/org/prebid/server/settings/model/GdprConfig.java @@ -7,7 +7,7 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Builder @AllArgsConstructor diff --git a/src/main/java/org/prebid/server/settings/model/Purpose.java b/src/main/java/org/prebid/server/settings/model/Purpose.java index 4e67c79503c..c193327b1be 100644 --- a/src/main/java/org/prebid/server/settings/model/Purpose.java +++ b/src/main/java/org/prebid/server/settings/model/Purpose.java @@ -1,6 +1,6 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -12,13 +12,13 @@ @AllArgsConstructor(staticName = "of") public class Purpose { - @JsonProperty("enforce-purpose") + @JsonAlias("enforce-purpose") EnforcePurpose enforcePurpose; - @JsonProperty("enforce-vendors") + @JsonAlias("enforce-vendors") Boolean enforceVendors; - @JsonProperty("vendor-exceptions") + @JsonAlias("vendor-exceptions") List vendorExceptions; PurposeEid eid; diff --git a/src/main/java/org/prebid/server/settings/model/PurposeEid.java b/src/main/java/org/prebid/server/settings/model/PurposeEid.java index 812c2b352ea..faa25b0b03c 100644 --- a/src/main/java/org/prebid/server/settings/model/PurposeEid.java +++ b/src/main/java/org/prebid/server/settings/model/PurposeEid.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -11,8 +12,10 @@ @AllArgsConstructor(staticName = "of") public class PurposeEid { + @JsonAlias("activity-transition") Boolean activityTransition; + @JsonAlias("require-consent") boolean requireConsent; Set exceptions; diff --git a/src/main/java/org/prebid/server/settings/model/PurposeOneTreatmentInterpretation.java b/src/main/java/org/prebid/server/settings/model/PurposeOneTreatmentInterpretation.java index e3a6495d3b5..d2c6fd2e758 100644 --- a/src/main/java/org/prebid/server/settings/model/PurposeOneTreatmentInterpretation.java +++ b/src/main/java/org/prebid/server/settings/model/PurposeOneTreatmentInterpretation.java @@ -1,12 +1,17 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; public enum PurposeOneTreatmentInterpretation { ignore, - @JsonProperty("no-access-allowed") + + @JsonProperty("no_access_allowed") + @JsonAlias("no-access-allowed") noAccessAllowed, - @JsonProperty("access-allowed") + + @JsonProperty("access_allowed") + @JsonAlias("access-allowed") accessAllowed } diff --git a/src/main/java/org/prebid/server/settings/model/SpecialFeature.java b/src/main/java/org/prebid/server/settings/model/SpecialFeature.java index f17d61d7552..fefe7239c4e 100644 --- a/src/main/java/org/prebid/server/settings/model/SpecialFeature.java +++ b/src/main/java/org/prebid/server/settings/model/SpecialFeature.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; @@ -15,7 +16,7 @@ public class SpecialFeature { @JsonProperty(defaultValue = "true") Boolean enforce; - @JsonProperty("vendor-exceptions") + @JsonAlias("vendor-exceptions") List vendorExceptions; } diff --git a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java index 0c36818bbf4..2506321e031 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java +++ b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model.activity.privacy; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; @@ -28,10 +29,12 @@ public static class Config { Set sids; - @JsonProperty("normalizeFlags") + @JsonProperty("normalize_flags") + @JsonAlias({"normalizeFlags", "normalize-flags"}) Boolean normalizeSections; - @JsonProperty("activityConfig") + @JsonProperty("activity_config") + @JsonAlias({"activityConfig", "activity-config"}) List activitiesConfigs; } @@ -40,7 +43,8 @@ public static class ActivityConfig { Set activities; - @JsonProperty("restrictIfTrue") + @JsonProperty("restrict_if_true") + @JsonAlias({"restrictIfTrue", "restrict-if-true"}) ObjectNode jsonLogicNode; } } diff --git a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java index 85653f4f004..fa51135791b 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java +++ b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.settings.model.activity.privacy; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import lombok.experimental.Accessors; @@ -23,7 +24,8 @@ public PrivacyModuleQualifier getCode() { @Value(staticConstructor = "of") public static class Config { - @JsonProperty("skipSids") + @JsonProperty("skip_sids") + @JsonAlias({"skipSids", "skip-sids"}) List skipSids; } } diff --git a/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityComponentRuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityComponentRuleConfig.java deleted file mode 100644 index ca5bd8cb884..00000000000 --- a/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityComponentRuleConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.settings.model.activity.rule; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; -import org.prebid.server.activity.ComponentType; - -import java.util.List; - -@Value(staticConstructor = "of") -public class AccountActivityComponentRuleConfig implements AccountActivityRuleConfig { - - Condition condition; - - Boolean allow; - - @Value(staticConstructor = "of") - public static class Condition { - - @JsonProperty("componentType") - List componentTypes; - - @JsonProperty("componentName") - List componentNames; - } -} diff --git a/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityConditionsRuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityConditionsRuleConfig.java new file mode 100644 index 00000000000..1f638b1afa2 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityConditionsRuleConfig.java @@ -0,0 +1,37 @@ +package org.prebid.server.settings.model.activity.rule; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.activity.ComponentType; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AccountActivityConditionsRuleConfig implements AccountActivityRuleConfig { + + Condition condition; + + Boolean allow; + + @Value(staticConstructor = "of") + public static class Condition { + + @JsonProperty("component_type") + @JsonAlias({"componentType", "component-type"}) + List componentTypes; + + @JsonProperty("component_name") + @JsonAlias({"componentName", "component-name"}) + List componentNames; + + @JsonProperty("gpp_sid") + @JsonAlias({"gppSid", "gpp-sid"}) + List sids; + + @JsonProperty("geo") + List geoCodes; + + String gpc; + } +} diff --git a/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityGeoRuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityGeoRuleConfig.java deleted file mode 100644 index ea69edc3001..00000000000 --- a/src/main/java/org/prebid/server/settings/model/activity/rule/AccountActivityGeoRuleConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.prebid.server.settings.model.activity.rule; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; -import org.prebid.server.activity.ComponentType; - -import java.util.List; - -@Value(staticConstructor = "of") -public class AccountActivityGeoRuleConfig implements AccountActivityRuleConfig { - - Condition condition; - - Boolean allow; - - @Value(staticConstructor = "of") - public static class Condition { - - @JsonProperty("componentType") - List componentTypes; - - @JsonProperty("componentName") - List componentNames; - - @JsonProperty("gppSid") - List sids; - - @JsonProperty("geo") - List geoCodes; - - String gpc; - } -} diff --git a/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityDefaultRuleConfigMatcher.java b/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityDefaultRuleConfigMatcher.java index c5f40940f3e..87b085105c1 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityDefaultRuleConfigMatcher.java +++ b/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityDefaultRuleConfigMatcher.java @@ -1,7 +1,7 @@ package org.prebid.server.settings.model.activity.rule.resolver; import com.fasterxml.jackson.databind.JsonNode; -import org.prebid.server.settings.model.activity.rule.AccountActivityComponentRuleConfig; +import org.prebid.server.settings.model.activity.rule.AccountActivityConditionsRuleConfig; import org.prebid.server.settings.model.activity.rule.AccountActivityRuleConfig; public class AccountActivityDefaultRuleConfigMatcher implements AccountActivityRuleConfigMatcher { @@ -13,6 +13,6 @@ public boolean matches(JsonNode ruleNode) { @Override public Class type() { - return AccountActivityComponentRuleConfig.class; + return AccountActivityConditionsRuleConfig.class; } } diff --git a/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityGeoRuleConfigMatcher.java b/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityGeoRuleConfigMatcher.java deleted file mode 100644 index a1ac2eed8e3..00000000000 --- a/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityGeoRuleConfigMatcher.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.prebid.server.settings.model.activity.rule.resolver; - -import com.fasterxml.jackson.databind.JsonNode; -import org.prebid.server.settings.model.activity.rule.AccountActivityGeoRuleConfig; -import org.prebid.server.settings.model.activity.rule.AccountActivityRuleConfig; - -public class AccountActivityGeoRuleConfigMatcher implements AccountActivityRuleConfigMatcher { - - @Override - public boolean matches(JsonNode ruleNode) { - final JsonNode conditionNode = isNotNullObjectNode(ruleNode) ? ruleNode.get("condition") : null; - return isNotNullObjectNode(conditionNode) - && (conditionNode.has("gppSid") - || conditionNode.has("geo") - || conditionNode.has("gpc")); - } - - private static boolean isNotNullObjectNode(JsonNode jsonNode) { - return jsonNode != null && jsonNode.isObject(); - } - - @Override - public Class type() { - return AccountActivityGeoRuleConfig.class; - } -} diff --git a/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityRuleConfigResolver.java b/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityRuleConfigResolver.java index f02acacb382..6472d59b32e 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityRuleConfigResolver.java +++ b/src/main/java/org/prebid/server/settings/model/activity/rule/resolver/AccountActivityRuleConfigResolver.java @@ -12,14 +12,13 @@ private AccountActivityRuleConfigResolver() { private static final List MATCHERS = List.of( new AccountActivityPrivacyModulesRuleConfigMatcher(), - new AccountActivityGeoRuleConfigMatcher(), new AccountActivityDefaultRuleConfigMatcher()); public static Class resolve(JsonNode ruleNode) { return MATCHERS.stream() .filter(matcher -> matcher.matches(ruleNode)) .findFirst() - .orElseGet(() -> MATCHERS.get(MATCHERS.size() - 1)) + .orElse(MATCHERS.getLast()) .type(); } } diff --git a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java new file mode 100644 index 00000000000..cc4b80ad870 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java @@ -0,0 +1,196 @@ +package org.prebid.server.settings.service; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.CacheNotificationListener; +import org.prebid.server.settings.helper.DatabaseStoredDataResultMapper; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.vertx.Initializable; +import org.prebid.server.vertx.database.DatabaseClient; + +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * Service that periodically calls database for stored request updates. + * If refreshRate is negative, then the data will never be refreshed. + *

+ * The Queries should return a ResultSet with the following columns and types: + *

+ * 1. id: string
+ * 2. data: JSON
+ * 3. type: string ("request" or "imp")
+ * 
+ * + *

+ * If data is empty or the JSON "null", then the ID will be invalidated (e.g. a deletion). + * If data is not empty, depending on TYPE, it should be put to corresponding map with ID as a key and DATA as value. + *

+ */ +public class DatabasePeriodicRefreshService implements Initializable { + + private static final Logger logger = LoggerFactory.getLogger(DatabasePeriodicRefreshService.class); + + /** + * Example of initialize query: + *
+     * SELECT id, requestData, type
+     * FROM stored_requests;
+     * 
+     * This query will be run once on startup to fetch _all_ known Stored Request data from the database.
+     */
+    private final String initQuery;
+    /**
+     * Example of update query:
+     * 
+     * SELECT id, requestData, type
+     * FROM stored_requests
+     * WHERE last_updated > ?;
+     * 
+     * The code will be run periodically to fetch updates from the database.
+     * Wildcard "?" would be used to pass last update date automatically.
+     */
+    private final String updateQuery;
+    private final long refreshPeriod;
+    private final long timeout;
+    private final MetricName cacheType;
+    private final CacheNotificationListener cacheNotificationListener;
+    private final Vertx vertx;
+    private final DatabaseClient databaseClient;
+    private final TimeoutFactory timeoutFactory;
+    private final Metrics metrics;
+    private final Clock clock;
+
+    private Instant lastUpdate;
+
+    public DatabasePeriodicRefreshService(String initQuery,
+                                          String updateQuery,
+                                          long refreshPeriod,
+                                          long timeout,
+                                          MetricName cacheType,
+                                          CacheNotificationListener cacheNotificationListener,
+                                          Vertx vertx,
+                                          DatabaseClient databaseClient,
+                                          TimeoutFactory timeoutFactory,
+                                          Metrics metrics,
+                                          Clock clock) {
+
+        this.initQuery = Objects.requireNonNull(StringUtils.stripToNull(initQuery));
+        this.updateQuery = Objects.requireNonNull(StringUtils.stripToNull(updateQuery));
+        this.refreshPeriod = refreshPeriod;
+        this.timeout = timeout;
+        this.cacheType = Objects.requireNonNull(cacheType);
+        this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener);
+        this.vertx = Objects.requireNonNull(vertx);
+        this.databaseClient = Objects.requireNonNull(databaseClient);
+        this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
+        this.metrics = Objects.requireNonNull(metrics);
+        this.clock = Objects.requireNonNull(clock);
+    }
+
+    @Override
+    public void initialize(Promise initializePromise) {
+        getAll();
+        if (refreshPeriod > 0) {
+            vertx.setPeriodic(refreshPeriod, aLong -> refresh());
+        }
+        initializePromise.tryComplete();
+    }
+
+    private void getAll() {
+        final long startTime = clock.millis();
+
+        databaseClient.executeQuery(
+                        initQuery,
+                        Collections.emptyList(),
+                        DatabaseStoredDataResultMapper::map,
+                        createTimeout())
+                .map(storedDataResult ->
+                        handleResult(storedDataResult, Instant.now(clock), startTime, MetricName.initialize))
+                .recover(exception -> handleFailure(exception, startTime, MetricName.initialize));
+    }
+
+    private Void handleResult(StoredDataResult storedDataResult,
+                              Instant updateTime,
+                              long startTime,
+                              MetricName refreshType) {
+
+        cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp());
+        lastUpdate = updateTime;
+
+        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
+
+        return null;
+    }
+
+    private Future handleFailure(Throwable exception, long startTime, MetricName refreshType) {
+        logger.warn("Error occurred while request to database refresh service", exception);
+
+        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
+        metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType);
+
+        return Future.failedFuture(exception);
+    }
+
+    private void refresh() {
+        final Instant updateTime = Instant.now(clock);
+        final long startTime = clock.millis();
+
+        databaseClient.executeQuery(
+                        updateQuery,
+                        Collections.singletonList(Date.from(lastUpdate)),
+                        DatabaseStoredDataResultMapper::map,
+                        createTimeout())
+                .map(storedDataResult ->
+                        handleResult(invalidate(storedDataResult), updateTime, startTime, MetricName.update))
+                .recover(exception -> handleFailure(exception, startTime, MetricName.update));
+    }
+
+    private StoredDataResult invalidate(StoredDataResult storedDataResult) {
+        final List invalidatedRequests = getInvalidatedKeys(storedDataResult.getStoredIdToRequest());
+        final List invalidatedImps = getInvalidatedKeys(storedDataResult.getStoredIdToImp());
+
+        if (!invalidatedRequests.isEmpty() || !invalidatedImps.isEmpty()) {
+            cacheNotificationListener.invalidate(invalidatedRequests, invalidatedImps);
+        }
+
+        final Map requestsToSave = removeFromMap(storedDataResult.getStoredIdToRequest(),
+                invalidatedRequests);
+        final Map impsToSave = removeFromMap(storedDataResult.getStoredIdToImp(), invalidatedImps);
+
+        return StoredDataResult.of(requestsToSave, impsToSave, storedDataResult.getErrors());
+    }
+
+    private static List getInvalidatedKeys(Map changesMap) {
+        return changesMap.entrySet().stream()
+                .filter(entry -> StringUtils.isBlank(entry.getValue())
+                        || StringUtils.equalsIgnoreCase(entry.getValue(), "null"))
+                .map(Map.Entry::getKey)
+                .toList();
+    }
+
+    private static Map removeFromMap(Map map, List invalidatedKeys) {
+        return map.entrySet().stream()
+                .filter(entry -> !invalidatedKeys.contains(entry.getKey()))
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
+    private Timeout createTimeout() {
+        return timeoutFactory.create(timeout);
+    }
+}
diff --git a/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java
index ef53df54ec2..e1b3bbc068f 100644
--- a/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java
+++ b/src/main/java/org/prebid/server/settings/service/HttpPeriodicRefreshService.java
@@ -4,19 +4,20 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import io.vertx.core.Future;
+import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
-import io.vertx.core.logging.Logger;
-import io.vertx.core.logging.LoggerFactory;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.settings.CacheNotificationListener;
 import org.prebid.server.settings.model.StoredDataType;
 import org.prebid.server.settings.proto.response.HttpRefreshResponse;
 import org.prebid.server.util.HttpUtil;
 import org.prebid.server.vertx.Initializable;
-import org.prebid.server.vertx.http.HttpClient;
-import org.prebid.server.vertx.http.model.HttpClientResponse;
+import org.prebid.server.vertx.httpclient.HttpClient;
+import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -90,11 +91,13 @@ public HttpPeriodicRefreshService(String refreshUrl,
     }
 
     @Override
-    public void initialize() {
+    public void initialize(Promise initializePromise) {
         getAll();
         if (refreshPeriod > 0) {
             vertx.setPeriodic(refreshPeriod, aLong -> refresh());
         }
+
+        initializePromise.tryComplete();
     }
 
     private void getAll() {
diff --git a/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java
deleted file mode 100644
index c479c2a0168..00000000000
--- a/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package org.prebid.server.settings.service;
-
-import io.vertx.core.Future;
-import io.vertx.core.Vertx;
-import io.vertx.core.logging.Logger;
-import io.vertx.core.logging.LoggerFactory;
-import org.apache.commons.lang3.StringUtils;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
-import org.prebid.server.metric.MetricName;
-import org.prebid.server.metric.Metrics;
-import org.prebid.server.settings.CacheNotificationListener;
-import org.prebid.server.settings.helper.JdbcStoredDataResultMapper;
-import org.prebid.server.settings.model.StoredDataResult;
-import org.prebid.server.vertx.Initializable;
-import org.prebid.server.vertx.jdbc.JdbcClient;
-
-import java.time.Clock;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * 

- * Service that periodically calls database for stored request updates. - * If refreshRate is negative, then the data will never be refreshed. - *

- * The Queries should return a ResultSet with the following columns and types: - *

- * 1. id: string
- * 2. data: JSON
- * 3. type: string ("request" or "imp")
- * 
- * - *

- * If data is empty or the JSON "null", then the ID will be invalidated (e.g. a deletion). - * If data is not empty, depending on TYPE, it should be put to corresponding map with ID as a key and DATA as value. - *

- */ -public class JdbcPeriodicRefreshService implements Initializable { - - private static final Logger logger = LoggerFactory.getLogger(JdbcPeriodicRefreshService.class); - - /** - * Example of initialize query: - *
-     * SELECT id, requestData, type
-     * FROM stored_requests;
-     * 
-     * This query will be run once on startup to fetch _all_ known Stored Request data from the database.
-     */
-    private final String initQuery;
-    /**
-     * Example of update query:
-     * 
-     * SELECT id, requestData, type
-     * FROM stored_requests
-     * WHERE last_updated > ?;
-     * 
-     * The code will be run periodically to fetch updates from the database.
-     * Wildcard "?" would be used to pass last update date automatically.
-     */
-    private final String updateQuery;
-    private final long refreshPeriod;
-    private final long timeout;
-    private final MetricName cacheType;
-    private final CacheNotificationListener cacheNotificationListener;
-    private final Vertx vertx;
-    private final JdbcClient jdbcClient;
-    private final TimeoutFactory timeoutFactory;
-    private final Metrics metrics;
-    private final Clock clock;
-
-    private Instant lastUpdate;
-
-    public JdbcPeriodicRefreshService(String initQuery,
-                                      String updateQuery,
-                                      long refreshPeriod,
-                                      long timeout,
-                                      MetricName cacheType,
-                                      CacheNotificationListener cacheNotificationListener,
-                                      Vertx vertx,
-                                      JdbcClient jdbcClient,
-                                      TimeoutFactory timeoutFactory,
-                                      Metrics metrics,
-                                      Clock clock) {
-
-        this.initQuery = Objects.requireNonNull(StringUtils.stripToNull(initQuery));
-        this.updateQuery = Objects.requireNonNull(StringUtils.stripToNull(updateQuery));
-        this.refreshPeriod = refreshPeriod;
-        this.timeout = timeout;
-        this.cacheType = Objects.requireNonNull(cacheType);
-        this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener);
-        this.vertx = Objects.requireNonNull(vertx);
-        this.jdbcClient = Objects.requireNonNull(jdbcClient);
-        this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
-        this.metrics = Objects.requireNonNull(metrics);
-        this.clock = Objects.requireNonNull(clock);
-    }
-
-    @Override
-    public void initialize() {
-        getAll();
-        if (refreshPeriod > 0) {
-            vertx.setPeriodic(refreshPeriod, aLong -> refresh());
-        }
-    }
-
-    private void getAll() {
-        final long startTime = clock.millis();
-
-        jdbcClient.executeQuery(
-                        initQuery,
-                        Collections.emptyList(),
-                        JdbcStoredDataResultMapper::map,
-                        createTimeout())
-                .map(storedDataResult ->
-                        handleResult(storedDataResult, Instant.now(clock), startTime, MetricName.initialize))
-                .recover(exception -> handleFailure(exception, startTime, MetricName.initialize));
-    }
-
-    private Void handleResult(StoredDataResult storedDataResult,
-                              Instant updateTime,
-                              long startTime,
-                              MetricName refreshType) {
-
-        cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp());
-        lastUpdate = updateTime;
-
-        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
-
-        return null;
-    }
-
-    private Future handleFailure(Throwable exception, long startTime, MetricName refreshType) {
-        logger.warn("Error occurred while request to jdbc refresh service", exception);
-
-        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
-        metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType);
-
-        return Future.failedFuture(exception);
-    }
-
-    private void refresh() {
-        final Instant updateTime = Instant.now(clock);
-        final long startTime = clock.millis();
-
-        jdbcClient.executeQuery(
-                        updateQuery,
-                        Collections.singletonList(Date.from(lastUpdate)),
-                        JdbcStoredDataResultMapper::map,
-                        createTimeout())
-                .map(storedDataResult ->
-                        handleResult(invalidate(storedDataResult), updateTime, startTime, MetricName.update))
-                .recover(exception -> handleFailure(exception, startTime, MetricName.update));
-    }
-
-    private StoredDataResult invalidate(StoredDataResult storedDataResult) {
-        final List invalidatedRequests = getInvalidatedKeys(storedDataResult.getStoredIdToRequest());
-        final List invalidatedImps = getInvalidatedKeys(storedDataResult.getStoredIdToImp());
-
-        if (!invalidatedRequests.isEmpty() || !invalidatedImps.isEmpty()) {
-            cacheNotificationListener.invalidate(invalidatedRequests, invalidatedImps);
-        }
-
-        final Map requestsToSave = removeFromMap(storedDataResult.getStoredIdToRequest(),
-                invalidatedRequests);
-        final Map impsToSave = removeFromMap(storedDataResult.getStoredIdToImp(), invalidatedImps);
-
-        return StoredDataResult.of(requestsToSave, impsToSave, storedDataResult.getErrors());
-    }
-
-    private static List getInvalidatedKeys(Map changesMap) {
-        return changesMap.entrySet().stream()
-                .filter(entry -> StringUtils.isBlank(entry.getValue())
-                        || StringUtils.equalsIgnoreCase(entry.getValue(), "null"))
-                .map(Map.Entry::getKey)
-                .toList();
-    }
-
-    private static Map removeFromMap(Map map, List invalidatedKeys) {
-        return map.entrySet().stream()
-                .filter(entry -> !invalidatedKeys.contains(entry.getKey()))
-                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-    }
-
-    private Timeout createTimeout() {
-        return timeoutFactory.create(timeout);
-    }
-}
diff --git a/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java
new file mode 100644
index 00000000000..d5a8ce7f873
--- /dev/null
+++ b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java
@@ -0,0 +1,146 @@
+package org.prebid.server.settings.service;
+
+import io.vertx.core.CompositeFuture;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import org.prebid.server.auction.model.Tuple2;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.metric.MetricName;
+import org.prebid.server.metric.Metrics;
+import org.prebid.server.settings.CacheNotificationListener;
+import org.prebid.server.settings.model.StoredDataResult;
+import org.prebid.server.vertx.Initializable;
+import software.amazon.awssdk.core.async.AsyncResponseTransformer;
+import software.amazon.awssdk.services.s3.S3AsyncClient;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsRequest;
+import software.amazon.awssdk.services.s3.model.S3Object;
+
+import java.time.Clock;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 

+ * Service that periodically calls s3 for stored request updates. + * If refreshRate is negative, then the data will never be refreshed. + *

+ * Fetches all files from the specified folders/prefixes in s3 and downloads all files. + */ +public class S3PeriodicRefreshService implements Initializable { + + private static final String JSON_SUFFIX = ".json"; + + private static final Logger logger = LoggerFactory.getLogger(S3PeriodicRefreshService.class); + + private final S3AsyncClient asyncClient; + private final String bucket; + private final String storedRequestsDirectory; + private final String storedImpressionsDirectory; + private final long refreshPeriod; + private final CacheNotificationListener cacheNotificationListener; + private final MetricName cacheType; + private final Clock clock; + private final Metrics metrics; + private final Vertx vertx; + + public S3PeriodicRefreshService(S3AsyncClient asyncClient, + String bucket, + String storedRequestsDirectory, + String storedImpressionsDirectory, + long refreshPeriod, + CacheNotificationListener cacheNotificationListener, + MetricName cacheType, + Clock clock, + Metrics metrics, + Vertx vertx) { + + this.asyncClient = Objects.requireNonNull(asyncClient); + this.bucket = Objects.requireNonNull(bucket); + this.storedRequestsDirectory = Objects.requireNonNull(storedRequestsDirectory); + this.storedImpressionsDirectory = Objects.requireNonNull(storedImpressionsDirectory); + this.refreshPeriod = refreshPeriod; + this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener); + this.cacheType = Objects.requireNonNull(cacheType); + this.clock = Objects.requireNonNull(clock); + this.metrics = Objects.requireNonNull(metrics); + this.vertx = Objects.requireNonNull(vertx); + } + + @Override + public void initialize(Promise initializePromise) { + fetchStoredDataResult(clock.millis(), MetricName.initialize) + .mapEmpty() + .onComplete(initializePromise); + + if (refreshPeriod > 0) { + logger.info("Starting s3 periodic refresh for " + cacheType + " every " + refreshPeriod + " s"); + vertx.setPeriodic(refreshPeriod, ignored -> fetchStoredDataResult(clock.millis(), MetricName.update)); + } + } + + private Future fetchStoredDataResult(long startTime, MetricName metricName) { + return Future.all( + getFileContentsForDirectory(storedRequestsDirectory), + getFileContentsForDirectory(storedImpressionsDirectory)) + .map(CompositeFuture::>list) + .map(results -> StoredDataResult.of(results.getFirst(), results.get(1), Collections.emptyList())) + .onSuccess(storedDataResult -> handleResult(storedDataResult, startTime, metricName)) + .onFailure(exception -> handleFailure(exception, startTime, metricName)); + } + + private Future> getFileContentsForDirectory(String directory) { + return listFiles(directory) + .map(files -> files.stream().map(this::downloadFile).toList()) + .compose(Future::all) + .map(CompositeFuture::>list) + .map(fileNameToContent -> fileNameToContent.stream() + .collect(Collectors.toMap( + entry -> stripFileName(directory, entry.getLeft()), + Tuple2::getRight))); + } + + private Future> listFiles(String prefix) { + final ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder() + .bucket(bucket) + .prefix(prefix) + .build(); + + return Future.fromCompletionStage(asyncClient.listObjects(listObjectsRequest), vertx.getOrCreateContext()) + .map(response -> response.contents().stream() + .map(S3Object::key) + .collect(Collectors.toList())); + } + + private Future> downloadFile(String key) { + final GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + return Future.fromCompletionStage( + asyncClient.getObject(request, AsyncResponseTransformer.toBytes()), + vertx.getOrCreateContext()) + .map(content -> Tuple2.of(key, content.asUtf8String())); + } + + private static String stripFileName(String directory, String name) { + return name + .replace(directory + "/", "") + .replace(JSON_SUFFIX, ""); + } + + private void handleResult(StoredDataResult storedDataResult, long startTime, MetricName refreshType) { + cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp()); + metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); + } + + private void handleFailure(Throwable exception, long startTime, MetricName refreshType) { + logger.warn("Error occurred while request to s3 refresh service", exception); + + metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); + metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java b/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java index c36bc2a1b20..486951c276f 100644 --- a/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ActivityInfrastructureConfiguration.java @@ -7,8 +7,7 @@ import org.prebid.server.activity.infrastructure.creator.privacy.uscustomlogic.USCustomLogicModuleCreator; import org.prebid.server.activity.infrastructure.creator.privacy.usnat.USNatGppReaderFactory; import org.prebid.server.activity.infrastructure.creator.privacy.usnat.USNatModuleCreator; -import org.prebid.server.activity.infrastructure.creator.rule.ComponentRuleCreator; -import org.prebid.server.activity.infrastructure.creator.rule.GeoRuleCreator; +import org.prebid.server.activity.infrastructure.creator.rule.ConditionsRuleCreator; import org.prebid.server.activity.infrastructure.creator.rule.PrivacyModulesRuleCreator; import org.prebid.server.activity.infrastructure.creator.rule.RuleCreator; import org.prebid.server.json.JacksonMapper; @@ -66,13 +65,8 @@ USCustomLogicModuleCreator usCustomLogicModuleCreator( static class RuleCreatorConfiguration { @Bean - ComponentRuleCreator componentRuleCreator() { - return new ComponentRuleCreator(); - } - - @Bean - GeoRuleCreator geoRuleCreator() { - return new GeoRuleCreator(); + ConditionsRuleCreator geoRuleCreator() { + return new ConditionsRuleCreator(); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java deleted file mode 100644 index 1bd29c31a3d..00000000000 --- a/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java +++ /dev/null @@ -1,311 +0,0 @@ -package org.prebid.server.spring.config; - -import com.codahale.metrics.MetricRegistry; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.AlertHttpService; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.DeliveryStatsService; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.PlannerService; -import org.prebid.server.deals.RegisterService; -import org.prebid.server.deals.simulation.DealsSimulationAdminHandler; -import org.prebid.server.handler.AccountCacheInvalidationHandler; -import org.prebid.server.handler.CollectedMetricsHandler; -import org.prebid.server.handler.CurrencyRatesHandler; -import org.prebid.server.handler.CustomizedAdminEndpoint; -import org.prebid.server.handler.DealsStatusHandler; -import org.prebid.server.handler.ForceDealsUpdateHandler; -import org.prebid.server.handler.HttpInteractionLogHandler; -import org.prebid.server.handler.LineItemStatusHandler; -import org.prebid.server.handler.LoggerControlKnobHandler; -import org.prebid.server.handler.SettingsCacheNotificationHandler; -import org.prebid.server.handler.TracerLogHandler; -import org.prebid.server.handler.VersionHandler; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.CriteriaManager; -import org.prebid.server.log.HttpInteractionLogger; -import org.prebid.server.log.LoggerControlKnob; -import org.prebid.server.settings.CachingApplicationSettings; -import org.prebid.server.settings.SettingsCache; -import org.prebid.server.util.VersionInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -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 org.springframework.stereotype.Component; - -import java.util.Collections; -import java.util.Map; - -@Configuration -public class AdminEndpointsConfiguration { - - @Bean - @ConditionalOnExpression("${admin-endpoints.version.enabled} == true") - CustomizedAdminEndpoint versionEndpoint( - VersionInfo versionInfo, - JacksonMapper mapper, - @Value("${admin-endpoints.version.path}") String path, - @Value("${admin-endpoints.version.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.version.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new VersionHandler(versionInfo.getVersion(), versionInfo.getCommitHash(), mapper, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${currency-converter.external-rates.enabled} == true" - + " and ${admin-endpoints.currency-rates.enabled} == true") - CustomizedAdminEndpoint currencyConversionRatesEndpoint( - CurrencyConversionService currencyConversionRates, - JacksonMapper mapper, - @Value("${admin-endpoints.currency-rates.path}") String path, - @Value("${admin-endpoints.currency-rates.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.currency-rates.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new CurrencyRatesHandler(currencyConversionRates, path, mapper), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" - + " and ${admin-endpoints.storedrequest.enabled} == true") - CustomizedAdminEndpoint cacheNotificationEndpoint( - SettingsCache settingsCache, - JacksonMapper mapper, - @Value("${admin-endpoints.storedrequest.path}") String path, - @Value("${admin-endpoints.storedrequest.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.storedrequest.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new SettingsCacheNotificationHandler(settingsCache, mapper, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" - + " and ${admin-endpoints.storedrequest-amp.enabled} == true") - CustomizedAdminEndpoint ampCacheNotificationEndpoint( - SettingsCache ampSettingsCache, - JacksonMapper mapper, - @Value("${admin-endpoints.storedrequest-amp.path}") String path, - @Value("${admin-endpoints.storedrequest-amp.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.storedrequest-amp.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new SettingsCacheNotificationHandler(ampSettingsCache, mapper, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" - + " and ${admin-endpoints.cache-invalidation.enabled} == true") - CustomizedAdminEndpoint cacheInvalidateNotificationEndpoint( - CachingApplicationSettings cachingApplicationSettings, - @Value("${admin-endpoints.cache-invalidation.path}") String path, - @Value("${admin-endpoints.cache-invalidation.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.cache-invalidation.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new AccountCacheInvalidationHandler(cachingApplicationSettings, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${admin-endpoints.logging-httpinteraction.enabled} == true") - CustomizedAdminEndpoint loggingHttpInteractionEndpoint( - @Value("${logging.http-interaction.max-limit}") int maxLimit, - HttpInteractionLogger httpInteractionLogger, - @Value("${admin-endpoints.logging-httpinteraction.path}") String path, - @Value("${admin-endpoints.logging-httpinteraction.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.logging-httpinteraction.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new HttpInteractionLogHandler(maxLimit, httpInteractionLogger, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${admin-endpoints.logging-changelevel.enabled} == true") - CustomizedAdminEndpoint loggingChangeLevelEndpoint( - @Value("${logging.change-level.max-duration-ms}") long maxDuration, - LoggerControlKnob loggerControlKnob, - @Value("${admin-endpoints.logging-changelevel.path}") String path, - @Value("${admin-endpoints.logging-changelevel.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.logging-changelevel.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new LoggerControlKnobHandler(maxDuration, loggerControlKnob, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnProperty(prefix = "admin-endpoints.tracelog", name = "enabled", havingValue = "true") - CustomizedAdminEndpoint tracerLogEndpoint( - CriteriaManager criteriaManager, - @Value("${admin-endpoints.tracelog.path}") String path, - @Value("${admin-endpoints.tracelog.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.tracelog.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new TracerLogHandler(criteriaManager), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${deals.enabled} == true and ${admin-endpoints.deals-status.enabled} == true") - CustomizedAdminEndpoint dealsStatusEndpoint( - DeliveryProgressService deliveryProgressService, - JacksonMapper mapper, - @Value("${admin-endpoints.deals-status.path}") String path, - @Value("${admin-endpoints.deals-status.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.deals-status.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new DealsStatusHandler(deliveryProgressService, mapper), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${deals.enabled} == true and ${admin-endpoints.lineitem-status.enabled} == true") - CustomizedAdminEndpoint lineItemStatusEndpoint( - DeliveryProgressService deliveryProgressService, - JacksonMapper mapper, - @Value("${admin-endpoints.lineitem-status.path}") String path, - @Value("${admin-endpoints.lineitem-status.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.lineitem-status.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new LineItemStatusHandler(deliveryProgressService, mapper, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${deals.enabled} == true and ${admin-endpoints.force-deals-update.enabled} == true") - CustomizedAdminEndpoint forceDealsUpdateEndpoint( - DeliveryStatsService deliveryStatsService, - PlannerService plannerService, - RegisterService registerService, - AlertHttpService alertHttpService, - DeliveryProgressService deliveryProgressService, - LineItemService lineItemService, - @Value("${admin-endpoints.force-deals-update.path}") String path, - @Value("${admin-endpoints.force-deals-update.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.force-deals-update.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new ForceDealsUpdateHandler( - deliveryStatsService, - plannerService, - registerService, - alertHttpService, - deliveryProgressService, - lineItemService, - path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == true" - + " and ${admin-endpoints.e2eadmin.enabled} == true") - CustomizedAdminEndpoint dealsSimulationAdminEndpoint( - DealsSimulationAdminHandler dealsSimulationAdminHandler, - @Value("${admin-endpoints.e2eadmin.path}") String path, - @Value("${admin-endpoints.e2eadmin.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.e2eadmin.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - dealsSimulationAdminHandler, - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - @ConditionalOnExpression("${admin-endpoints.collected-metrics.enabled} == true") - CustomizedAdminEndpoint collectedMetricsAdminEndpoint( - MetricRegistry metricRegistry, - JacksonMapper mapper, - @Value("${admin-endpoints.collected-metrics.path}") String path, - @Value("${admin-endpoints.collected-metrics.on-application-port}") boolean isOnApplicationPort, - @Value("${admin-endpoints.collected-metrics.protected}") boolean isProtected, - @Autowired(required = false) Map adminEndpointCredentials) { - - return new CustomizedAdminEndpoint( - path, - new CollectedMetricsHandler(metricRegistry, mapper, path), - isOnApplicationPort, - isProtected) - .withCredentials(adminEndpointCredentials); - } - - @Bean - Map adminEndpointCredentials( - @Autowired(required = false) AdminEndpointCredentials adminEndpointCredentials) { - - return ObjectUtils.defaultIfNull(adminEndpointCredentials.getCredentials(), Collections.emptyMap()); - } - - @Component - @ConfigurationProperties(prefix = "admin-endpoints") - @Data - @NoArgsConstructor - public static class AdminEndpointCredentials { - - private Map credentials; - } -} diff --git a/src/main/java/org/prebid/server/spring/config/AdminServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/AdminServerConfiguration.java deleted file mode 100644 index 4cc83bdfc5d..00000000000 --- a/src/main/java/org/prebid/server/spring/config/AdminServerConfiguration.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.prebid.server.spring.config; - -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import org.prebid.server.handler.CustomizedAdminEndpoint; -import org.prebid.server.vertx.ContextRunner; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import javax.annotation.PostConstruct; -import java.util.List; - -@Configuration -@ConditionalOnProperty(prefix = "admin", name = "port") -public class AdminServerConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(AdminServerConfiguration.class); - - @Autowired - private ContextRunner contextRunner; - - @Autowired - private Vertx vertx; - - @Autowired - @Qualifier("adminRouter") - private Router adminRouter; - - @Value("${admin.port}") - private int adminPort; - - @Bean(name = "adminRouter") - Router adminRouter(BodyHandler bodyHandler, List customizedAdminEndpoints) { - final Router router = Router.router(vertx); - router.route().handler(bodyHandler); - - customizedAdminEndpoints.stream() - .filter(customizedAdminEndpoint -> !customizedAdminEndpoint.isOnApplicationPort()) - .forEach(customizedAdminEndpoint -> customizedAdminEndpoint.router(router)); - - return router; - } - - @PostConstruct - public void startAdminServer() { - logger.info("Starting Admin Server to serve requests on port {0,number,#}", adminPort); - - contextRunner.runOnServiceContext(future -> - vertx.createHttpServer().requestHandler(adminRouter).listen(adminPort, future)); - - logger.info("Successfully started Admin Server"); - } -} diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java index 46b98597121..d618c36fa36 100644 --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java @@ -4,8 +4,14 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.analytics.reporter.agma.AgmaAnalyticsReporter; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; +import org.prebid.server.analytics.reporter.greenbids.GreenbidsAnalyticsReporter; +import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; import org.prebid.server.analytics.reporter.log.LogAnalyticsReporter; import org.prebid.server.analytics.reporter.pubstack.PubstackAnalyticsReporter; import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; @@ -13,7 +19,8 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.httpclient.HttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -22,8 +29,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.Clock; import java.util.List; +import java.util.Set; +import java.util.Map; +import java.util.stream.Collectors; @Configuration public class AnalyticsConfiguration { @@ -35,7 +47,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( TcfEnforcement tcfEnforcement, UserFpdActivityMask userFpdActivityMask, Metrics metrics, - @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + @Value("${logging.sampling-rate:0.01}") double logSamplingRate, + @Value("${analytics.global.adapters}") Set globalEnabledAdapters, + JacksonMapper mapper) { return new AnalyticsReporterDelegator( vertx, @@ -43,7 +57,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( tcfEnforcement, userFpdActivityMask, metrics, - logSamplingRate); + logSamplingRate, + globalEnabledAdapters, + mapper); } @Bean @@ -52,6 +68,166 @@ LogAnalyticsReporter logAnalyticsReporter(JacksonMapper mapper) { return new LogAnalyticsReporter(mapper); } + @Configuration + @ConditionalOnProperty(prefix = "analytics.agma", name = "enabled", havingValue = "true") + public static class AgmaAnalyticsConfiguration { + + @Bean + AgmaAnalyticsReporter agmaAnalyticsReporter(AgmaAnalyticsConfigurationProperties properties, + JacksonMapper jacksonMapper, + HttpClient httpClient, + Clock clock, + PrebidVersionProvider prebidVersionProvider, + Vertx vertx) { + + return new AgmaAnalyticsReporter( + properties.toComponentProperties(), + prebidVersionProvider, + jacksonMapper, + clock, + httpClient, + vertx); + } + + @Bean + @ConfigurationProperties(prefix = "analytics.agma") + AgmaAnalyticsConfigurationProperties agmaAnalyticsConfigurationProperties() { + return new AgmaAnalyticsConfigurationProperties(); + } + + @Validated + @NoArgsConstructor + @Data + private static class AgmaAnalyticsConfigurationProperties { + + @NotNull + private AgmaAnalyticsHttpEndpointProperties endpoint; + + @NotNull + private AgmaAnalyticsBufferProperties buffers; + + @NotEmpty(message = "Please configure at least one account for Agma Analytics") + private List accounts; + + public AgmaAnalyticsProperties toComponentProperties() { + final Map accountsByPublisherId = accounts.stream() + .collect(Collectors.toMap( + this::buildPublisherSiteAppIdKey, + AgmaAnalyticsAccountProperties::getCode + )); + + return AgmaAnalyticsProperties.builder() + .url(endpoint.getUrl()) + .gzip(BooleanUtils.isTrue(endpoint.getGzip())) + .bufferSize(buffers.getSizeBytes()) + .maxEventsCount(buffers.getCount()) + .bufferTimeoutMs(buffers.getTimeoutMs()) + .httpTimeoutMs(endpoint.getTimeoutMs()) + .accounts(accountsByPublisherId) + .build(); + } + + private String buildPublisherSiteAppIdKey(AgmaAnalyticsAccountProperties account) { + final String publisherId = account.getPublisherId(); + final String siteAppId = account.getSiteAppId(); + return StringUtils.isNotBlank(siteAppId) + ? String.format("%s_%s", publisherId, siteAppId) + : publisherId; + } + + @Validated + @NoArgsConstructor + @Data + private static class AgmaAnalyticsHttpEndpointProperties { + + @NotNull + private String url; + + @NotNull + private Long timeoutMs; + + private Boolean gzip; + } + + @NoArgsConstructor + @Data + private static class AgmaAnalyticsBufferProperties { + + @NotNull + private Integer sizeBytes; + + @NotNull + private Integer count; + + @NotNull + private Long timeoutMs; + } + + @NoArgsConstructor + @Data + private static class AgmaAnalyticsAccountProperties { + + private String code; + + @NotNull + private String publisherId; + + private String siteAppId; + } + } + } + + @Configuration + @ConditionalOnProperty(prefix = "analytics.greenbids", name = "enabled", havingValue = "true") + public static class GreenbidsAnalyticsConfiguration { + + @Bean + GreenbidsAnalyticsReporter greenbidsAnalyticsReporter( + GreenbidsAnalyticsConfigurationProperties greenbidsAnalyticsConfigurationProperties, + JacksonMapper jacksonMapper, + HttpClient httpClient, + Clock clock, + PrebidVersionProvider prebidVersionProvider) { + return new GreenbidsAnalyticsReporter( + greenbidsAnalyticsConfigurationProperties.toComponentProperties(), + jacksonMapper, + httpClient, + clock, + prebidVersionProvider); + } + + @Bean + @ConfigurationProperties(prefix = "analytics.greenbids") + GreenbidsAnalyticsConfigurationProperties greenbidsAnalyticsConfigurationProperties() { + return new GreenbidsAnalyticsConfigurationProperties(); + } + + @Validated + @NoArgsConstructor + @Data + private static class GreenbidsAnalyticsConfigurationProperties { + String analyticsServerVersion; + + String analyticsServer; + + Double exploratorySamplingSplit; + + Double defaultSamplingRate; + + Long timeoutMs; + + public GreenbidsAnalyticsProperties toComponentProperties() { + return GreenbidsAnalyticsProperties.builder() + .exploratorySamplingSplit(getExploratorySamplingSplit()) + .defaultSamplingRate(getDefaultSamplingRate()) + .analyticsServerVersion(getAnalyticsServerVersion()) + .analyticsServerUrl(getAnalyticsServer()) + .timeoutMs(getTimeoutMs()) + .build(); + } + } + } + @Configuration @ConditionalOnProperty(prefix = "analytics.pubstack", name = "enabled", havingValue = "true") public static class PubstackAnalyticsConfiguration { diff --git a/src/main/java/org/prebid/server/spring/config/AopConfiguration.java b/src/main/java/org/prebid/server/spring/config/AopConfiguration.java deleted file mode 100644 index cdad8883732..00000000000 --- a/src/main/java/org/prebid/server/spring/config/AopConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.prebid.server.spring.config; - -import io.vertx.core.Future; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.prebid.server.health.HealthMonitor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; - -@Configuration -public class AopConfiguration { - - @Bean - HealthMonitor healthMonitor() { - return new HealthMonitor(); - } - - @Aspect - @Component - static class HealthMonitorAspect { - - @Autowired - HealthMonitor healthMonitor; - - @Around(value = "execution(* org.prebid.server.vertx.http.HttpClient.*(..)) " - + "|| execution(* org.prebid.server.settings.ApplicationSettings.*(..)) " - + "|| execution(* org.prebid.server.geolocation.GeoLocationService.*(..))") - public Future around(ProceedingJoinPoint joinPoint) { - try { - return ((Future) joinPoint.proceed()) - .map(this::handleSucceedRequest) - .recover(this::handleFailRequest); - } catch (Throwable e) { - throw new IllegalStateException("Error while processing health monitoring", e); - } - } - - private Future handleFailRequest(Throwable throwable) { - healthMonitor.incTotal(); - return Future.failedFuture(throwable); - } - - private T handleSucceedRequest(T result) { - healthMonitor.incTotal(); - healthMonitor.incSuccess(); - return result; - } - } -} diff --git a/src/main/java/org/prebid/server/spring/config/DealsConfiguration.java b/src/main/java/org/prebid/server/spring/config/DealsConfiguration.java deleted file mode 100644 index 5b935ca1602..00000000000 --- a/src/main/java/org/prebid/server/spring/config/DealsConfiguration.java +++ /dev/null @@ -1,914 +0,0 @@ -package org.prebid.server.spring.config; - -import io.vertx.core.Vertx; -import io.vertx.core.eventbus.EventBus; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.bidder.BidderErrorNotifier; -import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory; -import org.prebid.server.bidder.DealsBidderRequestCompletionTrackerFactory; -import org.prebid.server.bidder.HttpBidderRequestEnricher; -import org.prebid.server.bidder.HttpBidderRequester; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.AdminCentralService; -import org.prebid.server.deals.AlertHttpService; -import org.prebid.server.deals.DealsService; -import org.prebid.server.deals.DeliveryProgressReportFactory; -import org.prebid.server.deals.DeliveryProgressService; -import org.prebid.server.deals.DeliveryStatsService; -import org.prebid.server.deals.LineItemService; -import org.prebid.server.deals.PlannerService; -import org.prebid.server.deals.RegisterService; -import org.prebid.server.deals.Suspendable; -import org.prebid.server.deals.TargetingService; -import org.prebid.server.deals.UserAdditionalInfoService; -import org.prebid.server.deals.UserService; -import org.prebid.server.deals.deviceinfo.DeviceInfoService; -import org.prebid.server.deals.events.AdminEventProcessor; -import org.prebid.server.deals.events.AdminEventService; -import org.prebid.server.deals.events.ApplicationEventProcessor; -import org.prebid.server.deals.events.ApplicationEventService; -import org.prebid.server.deals.events.EventServiceInitializer; -import org.prebid.server.deals.simulation.DealsSimulationAdminHandler; -import org.prebid.server.deals.simulation.SimulationAwareDeliveryProgressService; -import org.prebid.server.deals.simulation.SimulationAwareDeliveryStatsService; -import org.prebid.server.deals.simulation.SimulationAwareHttpBidderRequester; -import org.prebid.server.deals.simulation.SimulationAwareLineItemService; -import org.prebid.server.deals.simulation.SimulationAwarePlannerService; -import org.prebid.server.deals.simulation.SimulationAwareRegisterService; -import org.prebid.server.deals.simulation.SimulationAwareUserService; -import org.prebid.server.geolocation.GeoLocationService; -import org.prebid.server.health.HealthMonitor; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.CriteriaLogManager; -import org.prebid.server.log.CriteriaManager; -import org.prebid.server.metric.Metrics; -import org.prebid.server.settings.CachingApplicationSettings; -import org.prebid.server.settings.SettingsCache; -import org.prebid.server.vertx.ContextRunner; -import org.prebid.server.vertx.http.HttpClient; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -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 org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.validation.annotation.Validated; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import java.time.Clock; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Configuration -public class DealsConfiguration { - - @Configuration - @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == false") - public static class ProductionConfiguration { - - @Bean - PlannerService plannerService( - PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - DeliveryProgressService deliveryProgressService, - LineItemService lineItemService, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - JacksonMapper mapper) { - - return new PlannerService( - plannerProperties.toComponentProperties(), - deploymentProperties.toComponentProperties(), - lineItemService, - deliveryProgressService, - alertHttpService, - httpClient, - metrics, - clock, - mapper); - } - - @Bean - RegisterService registerService( - PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - AdminEventService adminEventService, - DeliveryProgressService deliveryProgressService, - AlertHttpService alertHttpService, - HealthMonitor healthMonitor, - CurrencyConversionService currencyConversionService, - HttpClient httpClient, - Vertx vertx, - JacksonMapper mapper) { - - return new RegisterService( - plannerProperties.toComponentProperties(), - deploymentProperties.toComponentProperties(), - adminEventService, - deliveryProgressService, - alertHttpService, - healthMonitor, - currencyConversionService, - httpClient, - vertx, - mapper); - } - - @Bean - DeliveryStatsService deliveryStatsService( - DeliveryStatsProperties deliveryStatsProperties, - DeliveryProgressReportFactory deliveryProgressReportFactory, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - Vertx vertx, - JacksonMapper mapper) { - - return new DeliveryStatsService( - deliveryStatsProperties.toComponentProperties(), - deliveryProgressReportFactory, - alertHttpService, - httpClient, - metrics, - clock, - vertx, - mapper); - } - - @Bean - LineItemService lineItemService( - @Value("${deals.max-deals-per-bidder}") int maxDealsPerBidder, - TargetingService targetingService, - CurrencyConversionService conversionService, - ApplicationEventService applicationEventService, - @Value("${auction.ad-server-currency}") String adServerCurrency, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - return new LineItemService(maxDealsPerBidder, - targetingService, - conversionService, - applicationEventService, - adServerCurrency, - clock, - criteriaLogManager); - } - - @Bean - DeliveryProgressService deliveryProgressService( - DeliveryProgressProperties deliveryProgressProperties, - LineItemService lineItemService, - DeliveryStatsService deliveryStatsService, - DeliveryProgressReportFactory deliveryProgressReportFactory, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - return new DeliveryProgressService( - deliveryProgressProperties.toComponentProperties(), - lineItemService, - deliveryStatsService, - deliveryProgressReportFactory, - clock, - criteriaLogManager); - } - - @Bean - UserService userService( - UserDetailsProperties userDetailsProperties, - @Value("${datacenter-region}") String dataCenterRegion, - LineItemService lineItemService, - HttpClient httpClient, - Clock clock, - Metrics metrics, - JacksonMapper mapper) { - - return new UserService( - userDetailsProperties.toComponentProperties(), - dataCenterRegion, - lineItemService, - httpClient, - clock, - metrics, - mapper); - } - } - - @Configuration - @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == false") - @EnableScheduling - public static class SchedulerConfiguration { - - @Bean - GeneralPlannerScheduler generalPlannerScheduler(PlannerService plannerService, - ContextRunner contextRunner) { - return new GeneralPlannerScheduler(plannerService, contextRunner); - } - - @Bean - @ConditionalOnExpression( - "'${deals.delivery-stats.delivery-period}'" - + ".equals('${deals.delivery-progress.report-reset-period}')") - ImmediateDeliveryScheduler immediateDeliveryScheduler(DeliveryProgressService deliveryProgressService, - DeliveryStatsService deliveryStatsService, - Clock clock, - ContextRunner contextRunner) { - return new ImmediateDeliveryScheduler(deliveryProgressService, deliveryStatsService, clock, - contextRunner); - } - - @Bean - @ConditionalOnExpression( - "not '${deals.delivery-stats.delivery-period}'" - + ".equals('${deals.delivery-progress.report-reset-period}')") - DeliveryScheduler deliveryScheduler(DeliveryProgressService deliveryProgressService, - DeliveryStatsService deliveryStatsService, - Clock clock, - ContextRunner contextRunner) { - return new DeliveryScheduler(deliveryProgressService, deliveryStatsService, clock, - contextRunner); - } - - @Bean - AdvancePlansScheduler advancePlansScheduler(LineItemService lineItemService, - ContextRunner contextRunner, - Clock clock) { - return new AdvancePlansScheduler(lineItemService, contextRunner, clock); - } - - private static class GeneralPlannerScheduler { - - private final PlannerService plannerService; - private final ContextRunner contextRunner; - - GeneralPlannerScheduler(PlannerService plannerService, ContextRunner contextRunner) { - this.plannerService = plannerService; - this.contextRunner = contextRunner; - } - - @Scheduled(cron = "${deals.planner.update-period}") - public void fetchPlansFromGeneralPlanner() { - contextRunner.runOnServiceContext(future -> { - plannerService.updateLineItemMetaData(); - future.complete(); - }); - } - } - - private static class AdvancePlansScheduler { - private final LineItemService lineItemService; - private final ContextRunner contextRunner; - private final Clock clock; - - AdvancePlansScheduler(LineItemService lineItemService, ContextRunner contextRunner, Clock clock) { - this.lineItemService = lineItemService; - this.contextRunner = contextRunner; - this.clock = clock; - } - - @Scheduled(cron = "${deals.planner.plan-advance-period}") - public void advancePlans() { - contextRunner.runOnServiceContext(future -> { - lineItemService.advanceToNextPlan(ZonedDateTime.now(clock)); - future.complete(); - }); - } - } - - private static class ImmediateDeliveryScheduler { - - private final DeliveryProgressService deliveryProgressService; - private final DeliveryStatsService deliveryStatsService; - private final Clock clock; - private final ContextRunner contextRunner; - - ImmediateDeliveryScheduler(DeliveryProgressService deliveryProgressService, - DeliveryStatsService deliveryStatsService, - Clock clock, - ContextRunner contextRunner) { - this.deliveryProgressService = deliveryProgressService; - this.deliveryStatsService = deliveryStatsService; - this.clock = clock; - this.contextRunner = contextRunner; - } - - @Scheduled(cron = "${deals.delivery-stats.delivery-period}") - public void createAndSendDeliveryReport() { - contextRunner.runOnServiceContext(future -> { - final ZonedDateTime now = ZonedDateTime.now(clock); - deliveryProgressService.createDeliveryProgressReports(now); - deliveryStatsService.sendDeliveryProgressReports(now); - future.complete(); - }); - } - } - } - - private static class DeliveryScheduler { - - private final DeliveryProgressService deliveryProgressService; - private final DeliveryStatsService deliveryStatsService; - private final Clock clock; - private final ContextRunner contextRunner; - - DeliveryScheduler(DeliveryProgressService deliveryProgressService, - DeliveryStatsService deliveryStatsService, - Clock clock, - ContextRunner contextRunner) { - this.deliveryProgressService = deliveryProgressService; - this.deliveryStatsService = deliveryStatsService; - this.clock = clock; - this.contextRunner = contextRunner; - } - - @Scheduled(cron = "${deals.delivery-progress.report-reset-period}") - public void createDeliveryReport() { - contextRunner.runOnServiceContext(future -> { - deliveryProgressService.createDeliveryProgressReports(ZonedDateTime.now(clock)); - future.complete(); - }); - } - - @Scheduled(cron = "${deals.delivery-stats.delivery-period}") - public void sendDeliveryReport() { - contextRunner.runOnServiceContext(future -> { - deliveryStatsService.sendDeliveryProgressReports(ZonedDateTime.now(clock)); - future.complete(); - }); - } - } - - @Configuration - @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == true") - public static class SimulationConfiguration { - - @Bean - @ConditionalOnProperty(prefix = "deals", name = "call-real-bidders-in-simulation", havingValue = "false", - matchIfMissing = true) - SimulationAwareHttpBidderRequester simulationAwareHttpBidderRequester( - HttpClient httpClient, - BidderRequestCompletionTrackerFactory completionTrackerFactory, - BidderErrorNotifier bidderErrorNotifier, - HttpBidderRequestEnricher requestEnricher, - LineItemService lineItemService, - JacksonMapper mapper) { - - return new SimulationAwareHttpBidderRequester( - httpClient, completionTrackerFactory, bidderErrorNotifier, requestEnricher, lineItemService, - mapper); - } - - @Bean - SimulationAwarePlannerService plannerService( - PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - DeliveryProgressService deliveryProgressService, - SimulationAwareLineItemService lineItemService, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - JacksonMapper mapper) { - - return new SimulationAwarePlannerService( - plannerProperties.toComponentProperties(), - deploymentProperties.toComponentProperties(), - lineItemService, - deliveryProgressService, - alertHttpService, - httpClient, - metrics, - clock, - mapper); - } - - @Bean - SimulationAwareRegisterService registerService( - PlannerProperties plannerProperties, - DeploymentProperties deploymentProperties, - AdminEventService adminEventService, - DeliveryProgressService deliveryProgressService, - AlertHttpService alertHttpService, - HealthMonitor healthMonitor, - CurrencyConversionService currencyConversionService, - HttpClient httpClient, - Vertx vertx, - JacksonMapper mapper) { - - return new SimulationAwareRegisterService( - plannerProperties.toComponentProperties(), - deploymentProperties.toComponentProperties(), - adminEventService, - deliveryProgressService, - alertHttpService, - healthMonitor, - currencyConversionService, - httpClient, - vertx, - mapper); - } - - @Bean - SimulationAwareDeliveryStatsService deliveryStatsService( - DeliveryStatsProperties deliveryStatsProperties, - DeliveryProgressReportFactory deliveryProgressReportFactory, - AlertHttpService alertHttpService, - HttpClient httpClient, - Metrics metrics, - Clock clock, - Vertx vertx, - JacksonMapper mapper) { - - return new SimulationAwareDeliveryStatsService( - deliveryStatsProperties.toComponentProperties(), - deliveryProgressReportFactory, - alertHttpService, - httpClient, - metrics, - clock, - vertx, - mapper); - } - - @Bean - SimulationAwareLineItemService lineItemService( - @Value("${deals.max-deals-per-bidder}") int maxDealsPerBidder, - TargetingService targetingService, - CurrencyConversionService conversionService, - ApplicationEventService applicationEventService, - @Value("${auction.ad-server-currency}") String adServerCurrency, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - return new SimulationAwareLineItemService( - maxDealsPerBidder, - targetingService, - conversionService, - applicationEventService, - adServerCurrency, - clock, - criteriaLogManager); - } - - @Bean - SimulationAwareDeliveryProgressService deliveryProgressService( - DeliveryProgressProperties deliveryProgressProperties, - LineItemService lineItemService, - DeliveryStatsService deliveryStatsService, - DeliveryProgressReportFactory deliveryProgressReportFactory, - @Value("${deals.simulation.ready-at-adjustment-ms}") long readyAtAdjustment, - Clock clock, - CriteriaLogManager criteriaLogManager) { - - return new SimulationAwareDeliveryProgressService( - deliveryProgressProperties.toComponentProperties(), - lineItemService, - deliveryStatsService, - deliveryProgressReportFactory, - readyAtAdjustment, - clock, - criteriaLogManager); - } - - @Bean - SimulationAwareUserService userService( - UserDetailsProperties userDetailsProperties, - SimulationProperties simulationProperties, - @Value("${datacenter-region}") String dataCenterRegion, - LineItemService lineItemService, - HttpClient httpClient, - Clock clock, - Metrics metrics, - JacksonMapper mapper) { - - return new SimulationAwareUserService( - userDetailsProperties.toComponentProperties(), - simulationProperties.toComponentProperties(), - dataCenterRegion, - lineItemService, - httpClient, - clock, - metrics, - mapper); - } - - @Bean - DealsSimulationAdminHandler dealsSimulationAdminHandler( - SimulationAwareRegisterService registerService, - SimulationAwarePlannerService plannerService, - SimulationAwareDeliveryProgressService deliveryProgressService, - SimulationAwareDeliveryStatsService deliveryStatsService, - @Autowired(required = false) SimulationAwareHttpBidderRequester httpBidderRequester, - JacksonMapper mapper, - @Value("${admin-endpoints.e2eadmin.path}") String path) { - - return new DealsSimulationAdminHandler( - registerService, - plannerService, - deliveryProgressService, - deliveryStatsService, - httpBidderRequester, - mapper, - path); - } - - @Bean - BeanPostProcessor simulationCustomizationBeanPostProcessor( - @Autowired(required = false) SimulationAwareHttpBidderRequester httpBidderRequester) { - - return new BeanPostProcessor() { - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - // there are HttpBidderRequester and SimulationAwareHttpBidderRequester in context by now, we would - // like to replace former with latter everywhere - if (httpBidderRequester != null && bean.getClass().isAssignableFrom(HttpBidderRequester.class) - && !(bean instanceof SimulationAwareHttpBidderRequester)) { - return httpBidderRequester; - } - - return bean; - } - }; - } - } - - @Configuration - @ConditionalOnExpression("${deals.enabled} == true") - public static class DealsMainConfiguration { - - @Bean - @ConfigurationProperties - DeploymentProperties deploymentProperties() { - return new DeploymentProperties(); - } - - @Bean - @ConfigurationProperties(prefix = "deals.planner") - PlannerProperties plannerProperties() { - return new PlannerProperties(); - } - - @Bean - @ConfigurationProperties(prefix = "deals.delivery-stats") - DeliveryStatsProperties deliveryStatsProperties() { - return new DeliveryStatsProperties(); - } - - @Bean - @ConfigurationProperties(prefix = "deals.delivery-progress") - DeliveryProgressProperties deliveryProgressProperties() { - return new DeliveryProgressProperties(); - } - - @Bean - @ConfigurationProperties(prefix = "deals.user-data") - UserDetailsProperties userDetailsProperties() { - return new UserDetailsProperties(); - } - - @Bean - @ConfigurationProperties(prefix = "deals.alert-proxy") - AlertProxyProperties alertProxyProperties() { - return new AlertProxyProperties(); - } - - @Bean - @ConfigurationProperties(prefix = "deals.simulation") - SimulationProperties simulationProperties() { - return new SimulationProperties(); - } - - @Bean - BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory() { - return new DealsBidderRequestCompletionTrackerFactory(); - } - - @Bean - UserAdditionalInfoService userAdditionalInfoService( - LineItemService lineItemService, - @Autowired(required = false) DeviceInfoService deviceInfoService, - @Autowired(required = false) GeoLocationService geoLocationService, - UserService userService, - Clock clock, - JacksonMapper mapper, - CriteriaLogManager criteriaLogManager) { - - return new UserAdditionalInfoService( - lineItemService, - deviceInfoService, - geoLocationService, - userService, - clock, - mapper, - criteriaLogManager); - } - - @Bean - DealsService dealsService(LineItemService lineItemService, - JacksonMapper mapper, - CriteriaLogManager criteriaLogManager) { - - return new DealsService(lineItemService, mapper, criteriaLogManager); - } - - @Bean - DeliveryProgressReportFactory deliveryProgressReportFactory( - DeploymentProperties deploymentProperties, - @Value("${deals.delivery-progress-report.competitors-number}") int competitorsNumber, - LineItemService lineItemService) { - - return new DeliveryProgressReportFactory( - deploymentProperties.toComponentProperties(), competitorsNumber, lineItemService); - } - - @Bean - AlertHttpService alertHttpService(JacksonMapper mapper, - HttpClient httpClient, - Clock clock, - DeploymentProperties deploymentProperties, - AlertProxyProperties alertProxyProperties) { - - return new AlertHttpService( - mapper, - httpClient, - clock, - deploymentProperties.toComponentProperties(), - alertProxyProperties.toComponentProperties()); - } - - @Bean - TargetingService targetingService(JacksonMapper mapper) { - return new TargetingService(mapper); - } - - @Bean - AdminCentralService adminCentralService( - CriteriaManager criteriaManager, - LineItemService lineItemService, - DeliveryProgressService deliveryProgressService, - @Autowired(required = false) @Qualifier("settingsCache") SettingsCache settingsCache, - @Autowired(required = false) @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache, - @Autowired(required = false) CachingApplicationSettings cachingApplicationSettings, - JacksonMapper mapper, - List suspendables) { - - return new AdminCentralService( - criteriaManager, - lineItemService, - deliveryProgressService, - settingsCache, - ampSettingsCache, - cachingApplicationSettings, - mapper, - suspendables); - } - - @Bean - ApplicationEventService applicationEventService(EventBus eventBus) { - return new ApplicationEventService(eventBus); - } - - @Bean - AdminEventService adminEventService(EventBus eventBus) { - return new AdminEventService(eventBus); - } - - @Bean - EventServiceInitializer eventServiceInitializer(List applicationEventProcessors, - List adminEventProcessors, - EventBus eventBus) { - - return new EventServiceInitializer(applicationEventProcessors, adminEventProcessors, eventBus); - } - } - - @Validated - @Data - @NoArgsConstructor - private static class DeploymentProperties { - - @NotBlank - private String hostId; - - @NotBlank - private String datacenterRegion; - - @NotBlank - private String vendor; - - @NotBlank - private String profile; - - @NotBlank - private String infra; - - @NotBlank - private String dataCenter; - - @NotBlank - private String system; - - @NotBlank - private String subSystem; - - public org.prebid.server.deals.model.DeploymentProperties toComponentProperties() { - return org.prebid.server.deals.model.DeploymentProperties.builder() - .pbsHostId(getHostId()).pbsRegion(getDatacenterRegion()).pbsVendor(getVendor()) - .profile(getProfile()).infra(getInfra()).dataCenter(getDataCenter()).system(getSystem()) - .subSystem(getSubSystem()).build(); - } - } - - @Validated - @Data - @NoArgsConstructor - private static class PlannerProperties { - - @NotBlank - private String planEndpoint; - @NotBlank - private String registerEndpoint; - @NotNull - private Long timeoutMs; - @NotNull - private Long registerPeriodSec; - @NotBlank - private String username; - @NotBlank - private String password; - - public org.prebid.server.deals.model.PlannerProperties toComponentProperties() { - return org.prebid.server.deals.model.PlannerProperties.builder() - .planEndpoint(getPlanEndpoint()) - .registerEndpoint(getRegisterEndpoint()) - .timeoutMs(getTimeoutMs()) - .registerPeriodSeconds(getRegisterPeriodSec()) - .username(getUsername()) - .password(getPassword()) - .build(); - } - } - - @Validated - @Data - @NoArgsConstructor - private static class DeliveryStatsProperties { - - @NotBlank - private String endpoint; - @NotNull - private Integer cachedReportsNumber; - @NotNull - private Long timeoutMs; - @NotNull - private Integer lineItemsPerReport; - @NotNull - private Integer reportsIntervalMs; - @NotNull - private Integer batchesIntervalMs; - @NotNull - private Boolean requestCompressionEnabled; - @NotBlank - private String username; - @NotBlank - private String password; - - public org.prebid.server.deals.model.DeliveryStatsProperties toComponentProperties() { - return org.prebid.server.deals.model.DeliveryStatsProperties.builder() - .endpoint(getEndpoint()) - .cachedReportsNumber(getCachedReportsNumber()) - .timeoutMs(getTimeoutMs()) - .lineItemsPerReport(getLineItemsPerReport()) - .reportsIntervalMs(getReportsIntervalMs()) - .batchesIntervalMs(getBatchesIntervalMs()) - .requestCompressionEnabled(getRequestCompressionEnabled()) - .username(getUsername()) - .password(getPassword()) - .build(); - } - } - - @Validated - @Data - @NoArgsConstructor - private static class DeliveryProgressProperties { - - @NotNull - private Long lineItemStatusTtlSec; - @NotNull - private Integer cachedPlansNumber; - - public org.prebid.server.deals.model.DeliveryProgressProperties toComponentProperties() { - return org.prebid.server.deals.model.DeliveryProgressProperties.of(getLineItemStatusTtlSec(), - getCachedPlansNumber()); - } - } - - @Validated - @Data - @NoArgsConstructor - private static class UserDetailsProperties { - - @NotBlank - private String userDetailsEndpoint; - @NotBlank - private String winEventEndpoint; - @NotNull - private Long timeout; - @NotNull - private List userIds; - - public org.prebid.server.deals.model.UserDetailsProperties toComponentProperties() { - final List componentUserIds = getUserIds().stream() - .map(DealsConfiguration.UserIdRule::toComponentProperties) - .toList(); - - return org.prebid.server.deals.model.UserDetailsProperties.of( - getUserDetailsEndpoint(), getWinEventEndpoint(), getTimeout(), componentUserIds); - } - } - - @Validated - @Data - @NoArgsConstructor - private static class AlertProxyProperties { - - @NotNull - private boolean enabled; - - @NotBlank - private String url; - - @NotNull - private Integer timeoutSec; - - Map alertTypes; - - @NotBlank - private String username; - - @NotBlank - private String password; - - public org.prebid.server.deals.model.AlertProxyProperties toComponentProperties() { - return org.prebid.server.deals.model.AlertProxyProperties.builder() - .enabled(isEnabled()).url(getUrl()).timeoutSec(getTimeoutSec()) - .alertTypes(ObjectUtils.defaultIfNull(getAlertTypes(), new HashMap<>())) - .username(getUsername()) - .password(getPassword()).build(); - } - } - - @Validated - @NoArgsConstructor - @Data - private static class UserIdRule { - - @NotBlank - private String type; - - @NotBlank - private String source; - - @NotBlank - private String location; - - org.prebid.server.deals.model.UserIdRule toComponentProperties() { - return org.prebid.server.deals.model.UserIdRule.of(getType(), getSource(), getLocation()); - } - } - - @Validated - @NoArgsConstructor - @Data - private static class SimulationProperties { - - @NotNull - boolean enabled; - - Boolean winEventsEnabled; - - Boolean userDetailsEnabled; - - org.prebid.server.deals.model.SimulationProperties toComponentProperties() { - return org.prebid.server.deals.model.SimulationProperties.builder() - .enabled(isEnabled()) - .winEventsEnabled(getWinEventsEnabled() != null ? getWinEventsEnabled() : false) - .userDetailsEnabled(getUserDetailsEnabled() != null ? getUserDetailsEnabled() : false) - .build(); - } - } -} diff --git a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java index 9d3c4e62a4b..7edc69d6176 100644 --- a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java @@ -3,9 +3,14 @@ import io.vertx.core.Vertx; import io.vertx.core.http.HttpClientOptions; import lombok.Data; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; import org.prebid.server.execution.RemoteFileSyncer; +import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; +import org.prebid.server.execution.retry.RetryPolicy; import org.prebid.server.geolocation.CircuitBreakerSecuredGeoLocationService; import org.prebid.server.geolocation.ConfigurationGeoLocationService; import org.prebid.server.geolocation.CountryCodeMapper; @@ -13,8 +18,10 @@ import org.prebid.server.geolocation.MaxMindGeoLocationService; import org.prebid.server.metric.Metrics; import org.prebid.server.spring.config.model.CircuitBreakerProperties; +import org.prebid.server.spring.config.model.ExponentialBackoffProperties; import org.prebid.server.spring.config.model.HttpClientProperties; import org.prebid.server.spring.config.model.RemoteFileSyncerProperties; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -34,6 +41,7 @@ import java.util.ArrayList; import java.util.List; +@Configuration public class GeoLocationConfiguration { @Configuration @@ -78,25 +86,48 @@ CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( } private GeoLocationService createGeoLocationService(RemoteFileSyncerProperties properties, Vertx vertx) { + final MaxMindGeoLocationService maxMindGeoLocationService = new MaxMindGeoLocationService(); final HttpClientProperties httpClientProperties = properties.getHttpClient(); final HttpClientOptions httpClientOptions = new HttpClientOptions() .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) .setMaxRedirects(httpClientProperties.getMaxRedirects()); final RemoteFileSyncer remoteFileSyncer = new RemoteFileSyncer( + maxMindGeoLocationService, properties.getDownloadUrl(), properties.getSaveFilepath(), properties.getTmpFilepath(), - FixedIntervalRetryPolicy.limited(properties.getRetryIntervalMs(), properties.getRetryCount()), + toRetryPolicy(properties), properties.getTimeoutMs(), properties.getUpdateIntervalMs(), vertx.createHttpClient(httpClientOptions), vertx); - final MaxMindGeoLocationService maxMindGeoLocationService = new MaxMindGeoLocationService(); - remoteFileSyncer.sync(maxMindGeoLocationService); + remoteFileSyncer.sync(); return maxMindGeoLocationService; } + + // TODO: remove after transition period + private static RetryPolicy toRetryPolicy(RemoteFileSyncerProperties properties) { + final Long retryIntervalMs = properties.getRetryIntervalMs(); + final Integer retryCount = properties.getRetryCount(); + final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount); + final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount) + || !fixedRetryPolicyDefined; + + if (!fixedRetryPolicyValid) { + throw new IllegalArgumentException("fixed interval retry policy is invalid"); + } + + final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry(); + return fixedRetryPolicyDefined + ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount) + : ExponentialBackoffRetryPolicy.of( + exponentialBackoffProperties.getDelayMillis(), + exponentialBackoffProperties.getMaxDelayMillis(), + exponentialBackoffProperties.getFactor(), + exponentialBackoffProperties.getJitter()); + } } @Configuration @@ -180,22 +211,31 @@ static class GeoInfo { } } - @Configuration - static class CountryCodeMapperConfiguration { + @Bean + public CountryCodeMapper countryCodeMapper(@Value("classpath:country-codes.csv") Resource countryCodes, + @Value("classpath:mcc-country-codes.csv") Resource mccCountryCodes) + throws IOException { - @Bean - public CountryCodeMapper countryCodeMapper(@Value("classpath:country-codes.csv") Resource countryCodes, - @Value("classpath:mcc-country-codes.csv") Resource mccCountryCodes) - throws IOException { + return new CountryCodeMapper(readCsv(countryCodes), readCsv(mccCountryCodes)); + } - return new CountryCodeMapper(readCsv(countryCodes), readCsv(mccCountryCodes)); - } + private String readCsv(Resource resource) throws IOException { + final Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8); + final String csv = FileCopyUtils.copyToString(reader); + reader.close(); + return csv; + } - private String readCsv(Resource resource) throws IOException { - final Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8); - final String csv = FileCopyUtils.copyToString(reader); - reader.close(); - return csv; - } + @Bean + GeoLocationServiceWrapper geoLocationServiceWrapper( + @Autowired(required = false) GeoLocationService geoLocationService, + Ortb2ImplicitParametersResolver implicitParametersResolver, + Metrics metrics) { + + return new GeoLocationServiceWrapper( + geoLocationService, + implicitParametersResolver, + metrics); } + } diff --git a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java index 9f6669141f8..93eaeb48030 100644 --- a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import io.vertx.ext.jdbc.JDBCClient; +import io.vertx.sqlclient.Pool; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.ApplicationChecker; @@ -24,10 +24,10 @@ public class HealthCheckerConfiguration { @Bean @ConditionalOnProperty(prefix = "health-check.database", name = "enabled", havingValue = "true") HealthChecker databaseChecker(Vertx vertx, - JDBCClient jdbcClient, + Pool pool, @Value("${health-check.database.refresh-period-ms}") long refreshPeriod) { - return new DatabaseHealthChecker(vertx, jdbcClient, refreshPeriod); + return new DatabaseHealthChecker(vertx, pool, refreshPeriod); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/InitializationConfiguration.java b/src/main/java/org/prebid/server/spring/config/InitializationConfiguration.java index a0b0498e16b..7bce3c5358e 100644 --- a/src/main/java/org/prebid/server/spring/config/InitializationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/InitializationConfiguration.java @@ -1,13 +1,14 @@ package org.prebid.server.spring.config; +import com.codahale.metrics.ScheduledReporter; import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.ContextRunner; import org.prebid.server.vertx.Initializable; -import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.verticles.VerticleDefinition; +import org.prebid.server.vertx.verticles.server.DaemonVerticle; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.EventListener; import java.util.List; @@ -27,17 +28,10 @@ @Configuration public class InitializationConfiguration { - @Autowired - private ContextRunner contextRunner; + @Bean + VerticleDefinition daemonVerticleDefinition(@Autowired(required = false) List initializables, + @Autowired(required = false) List reporters) { - @Autowired - private List initializables; - - @EventListener(ContextRefreshedEvent.class) - public void initializeServices() { - contextRunner.runOnServiceContext(promise -> { - initializables.forEach(Initializable::initialize); - promise.complete(); - }); + return VerticleDefinition.ofSingleInstance(() -> new DaemonVerticle(initializables, reporters)); } } diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java index 67b4ce379ea..79a62ffbe87 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -8,6 +8,7 @@ import org.prebid.server.floors.BasicPriceFloorEnforcer; import org.prebid.server.floors.BasicPriceFloorProcessor; import org.prebid.server.floors.BasicPriceFloorResolver; +import org.prebid.server.floors.NoSignalBidderPriceFloorAdjuster; import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorFetcher; @@ -18,11 +19,12 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.httpclient.HttpClient; 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 org.springframework.context.annotation.Primary; @Configuration public class PriceFloorsConfiguration { @@ -51,8 +53,9 @@ PriceFloorFetcher priceFloorFetcher( @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, + PriceFloorAdjuster priceFloorAdjuster, Metrics metrics) { - return new BasicPriceFloorEnforcer(currencyConversionService, metrics); + return new BasicPriceFloorEnforcer(currencyConversionService, priceFloorAdjuster, metrics); } @Bean @@ -93,16 +96,24 @@ PriceFloorProcessor noOpPriceFloorProcessor() { } @Bean + @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") FloorAdjustmentFactorResolver floorsAdjustmentFactorResolver() { return new FloorAdjustmentFactorResolver(); } @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") - PriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { + BasicPriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver); } + @Bean + @Primary + @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") + PriceFloorAdjuster noSignalBidderPriceFloorAdjuster(BasicPriceFloorAdjuster basicPriceFloorAdjuster) { + return new NoSignalBidderPriceFloorAdjuster(basicPriceFloorAdjuster); + } + @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "false", matchIfMissing = true) PriceFloorAdjuster noOpPriceFloorAdjuster() { diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java index 1a6a78a5c42..c7cac1ecf64 100644 --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java @@ -3,6 +3,7 @@ import io.vertx.core.Vertx; import io.vertx.core.file.FileSystem; import lombok.Data; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.privacy.enforcement.ActivityEnforcement; import org.prebid.server.auction.privacy.enforcement.CcpaEnforcement; @@ -13,7 +14,6 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdTcfMask; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.HostVendorTcfDefinerService; @@ -45,17 +45,16 @@ import org.prebid.server.settings.model.SpecialFeature; import org.prebid.server.settings.model.SpecialFeatures; import org.prebid.server.spring.config.retry.RetryPolicyConfigurationProperties; -import org.prebid.server.vertx.http.HttpClient; -import org.springframework.beans.factory.annotation.Autowired; +import org.prebid.server.vertx.httpclient.HttpClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.util.Arrays; import java.util.HashSet; @@ -162,10 +161,11 @@ TcfDefinerService tcfDefinerService( GdprConfig gdprConfig, @Value("${gdpr.eea-countries}") String eeaCountriesAsString, Tcf2Service tcf2Service, - @Autowired(required = false) GeoLocationService geoLocationService, + GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, - Metrics metrics) { + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double samplingRate) { final Set eeaCountries = new HashSet<>(Arrays.asList(eeaCountriesAsString.trim().split(","))); @@ -173,10 +173,11 @@ TcfDefinerService tcfDefinerService( gdprConfig, eeaCountries, tcf2Service, - geoLocationService, + geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + samplingRate); } @Bean @@ -356,13 +357,13 @@ UserFpdActivityMask userFpdActivityMask(UserFpdTcfMask userFpdTcfMask) { } @Bean - UserFpdCcpaMask userFpdCcpaMask(IpAddressHelper ipAddressHelper) { - return new UserFpdCcpaMask(ipAddressHelper); + UserFpdCcpaMask userFpdCcpaMask(UserFpdActivityMask userFpdActivityMask) { + return new UserFpdCcpaMask(userFpdActivityMask); } @Bean - UserFpdCoppaMask userFpdCoppaMask(IpAddressHelper ipAddressHelper) { - return new UserFpdCoppaMask(ipAddressHelper); + UserFpdCoppaMask userFpdCoppaMask(UserFpdActivityMask userFpdActivityMask) { + return new UserFpdCoppaMask(userFpdActivityMask); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 425396a667c..71e014fcc23 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -5,24 +5,27 @@ import io.vertx.core.Vertx; import io.vertx.core.file.FileSystem; import io.vertx.core.http.HttpClientOptions; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.core.net.JksOptions; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.BidResponseCreator; import org.prebid.server.auction.BidResponsePostProcessor; +import org.prebid.server.auction.BidsAdjuster; import org.prebid.server.auction.DebugResolver; import org.prebid.server.auction.DsaEnforcer; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.ImpAdjuster; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.SecBrowsingTopicsResolver; +import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.StoredResponseProcessor; import org.prebid.server.auction.SupplyChainResolver; @@ -69,17 +72,17 @@ import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory; import org.prebid.server.bidder.HttpBidderRequestEnricher; import org.prebid.server.bidder.HttpBidderRequester; -import org.prebid.server.cache.CacheService; +import org.prebid.server.cache.BasicPbcStorageService; +import org.prebid.server.cache.CoreCacheService; +import org.prebid.server.cache.PbcStorageService; import org.prebid.server.cache.model.CacheTtl; +import org.prebid.server.cache.utils.CacheServiceUtil; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.cookie.CookieSyncService; import org.prebid.server.cookie.CoopSyncProvider; import org.prebid.server.cookie.PrioritizedCoopSyncProvider; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.deals.DealsService; -import org.prebid.server.deals.UserAdditionalInfoService; -import org.prebid.server.deals.events.ApplicationEventService; import org.prebid.server.events.EventsService; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; @@ -91,7 +94,6 @@ import org.prebid.server.identity.IdGenerator; import org.prebid.server.identity.NoneIdGenerator; import org.prebid.server.identity.UUIDIdGenerator; -import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.log.CriteriaLogManager; @@ -104,7 +106,6 @@ import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties; @@ -112,14 +113,15 @@ import org.prebid.server.util.VersionInfo; import org.prebid.server.util.system.CpuLoadAverageStats; import org.prebid.server.validation.BidderParamValidator; +import org.prebid.server.validation.ImpValidator; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.ResponseBidValidator; import org.prebid.server.validation.VideoRequestValidator; import org.prebid.server.vast.VastModifier; import org.prebid.server.version.PrebidVersionProvider; -import org.prebid.server.vertx.http.BasicHttpClient; -import org.prebid.server.vertx.http.CircuitBreakerSecuredHttpClient; -import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.httpclient.BasicHttpClient; +import org.prebid.server.vertx.httpclient.CircuitBreakerSecuredHttpClient; +import org.prebid.server.vertx.httpclient.HttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -130,7 +132,7 @@ import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; import java.io.IOException; import java.time.Clock; import java.util.ArrayList; @@ -148,20 +150,18 @@ @Configuration public class ServiceConfiguration { - private static final Logger logger = LoggerFactory.getLogger(ServiceConfiguration.class); - @Value("${logging.sampling-rate:0.01}") private double logSamplingRate; @Bean - CacheService cacheService( + CoreCacheService cacheService( @Value("${cache.scheme}") String scheme, @Value("${cache.host}") String host, @Value("${cache.path}") String path, @Value("${cache.query}") String query, - @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl, - @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl, @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, + @Value("${pbc.api.key:#{null}}") String apiKey, + @Value("${cache.api-key-secured:false}") boolean apiKeySecured, VastModifier vastModifier, EventsService eventsService, HttpClient httpClient, @@ -169,12 +169,13 @@ CacheService cacheService( Clock clock, JacksonMapper mapper) { - return new CacheService( - CacheTtl.of(bannerCacheTtl, videoCacheTtl), + return new CoreCacheService( httpClient, - CacheService.getCacheEndpointUrl(scheme, host, path), - CacheService.getCachedAssetUrlTemplate(scheme, host, path, query), + CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), + CacheServiceUtil.getCachedAssetUrlTemplate(scheme, host, path, query), expectedCacheTimeMs, + apiKey, + apiKeySecured, vastModifier, eventsService, metrics, @@ -183,6 +184,31 @@ CacheService cacheService( mapper); } + @Bean + @ConditionalOnProperty(prefix = "cache.module", name = "enabled", havingValue = "false", matchIfMissing = true) + PbcStorageService noOpModuleCacheService() { + return PbcStorageService.noOp(); + } + + @Bean + @ConditionalOnProperty(prefix = "cache.module", name = "enabled", havingValue = "true") + PbcStorageService basicModuleCacheService( + @Value("${cache.scheme}") String scheme, + @Value("${cache.host}") String host, + @Value("${storage.pbc.path}") String path, + @Value("${storage.pbc.call-timeout-ms}") int callTimeoutMs, + @Value("${pbc.api.key}") String apiKey, + HttpClient httpClient, + JacksonMapper mapper) { + + return new BasicPbcStorageService( + httpClient, + CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), + apiKey, + callTimeoutMs, + mapper); + } + @Bean VastModifier vastModifier(BidderCatalog bidderCatalog, EventsService eventsService, Metrics metrics) { return new VastModifier(bidderCatalog, eventsService, metrics); @@ -226,6 +252,11 @@ FpdResolver fpdResolver(JacksonMapper mapper, JsonMerger jsonMerger) { return new FpdResolver(mapper, jsonMerger); } + @Bean + ImpAdjuster impAdjuster(ImpValidator impValidator, JacksonMapper jacksonMapper, JsonMerger jsonMerger) { + return new ImpAdjuster(jacksonMapper, jsonMerger, impValidator); + } + @Bean OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { return new OrtbTypesResolver(logSamplingRate, jacksonMapper, jsonMerger); @@ -242,24 +273,10 @@ SupplyChainResolver schainResolver( @Bean TimeoutResolver auctionTimeoutResolver( @Value("${auction.biddertmax.min}") long minTimeout, - @Value("${auction.max-timeout-ms:#{0}}") long maxTimeoutDeprecated, @Value("${auction.biddertmax.max:#{0}}") long maxTimeout, @Value("${auction.tmax-upstream-response-time}") long upstreamResponseTime) { - return new TimeoutResolver( - minTimeout, - resolveMaxTimeout(maxTimeoutDeprecated, maxTimeout), - upstreamResponseTime); - } - - // TODO: Remove after transition period - private static long resolveMaxTimeout(long maxTimeoutDeprecated, long maxTimeout) { - if (maxTimeout != 0) { - return maxTimeout; - } - - logger.warn("Usage of deprecated property: auction.max-timeout-ms. Use auction.biddertmax.max instead."); - return maxTimeoutDeprecated; + return new TimeoutResolver(minTimeout, maxTimeout, upstreamResponseTime); } @Bean @@ -280,10 +297,11 @@ Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver( @Value("${auction.cache.only-winning-bids}") boolean cacheOnlyWinningBids, @Value("${settings.generate-storedrequest-bidrequest-id}") boolean generateBidRequestId, @Value("${auction.ad-server-currency}") String adServerCurrency, - @Value("${auction.blacklisted-apps}") String blacklistedAppsString, + @Value("${auction.blocklisted-apps}") String blocklistedAppsString, @Value("${external-url}") String externalUrl, @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId, @Value("${datacenter-region}") String datacenterRegion, + BidderCatalog bidderCatalog, ImplicitParametersExtractor implicitParametersExtractor, TimeoutResolver timeoutResolver, IpAddressHelper ipAddressHelper, @@ -296,10 +314,11 @@ Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver( cacheOnlyWinningBids, generateBidRequestId, adServerCurrency, - splitToList(blacklistedAppsString), + splitToList(blocklistedAppsString), externalUrl, hostVendorId, datacenterRegion, + bidderCatalog, implicitParametersExtractor, timeoutResolver, ipAddressHelper, @@ -358,9 +377,8 @@ SetuidGppService setuidGppService(GppService gppService) { @Bean Ortb2RequestFactory openRtb2RequestFactory( - @Value("${settings.enforce-valid-account}") boolean enforceValidAccount, @Value("${auction.biddertmax.percent}") int timeoutAdjustmentFactor, - @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString, + @Value("${auction.blocklisted-accounts}") String blocklistedAccountsString, UidsCookieService uidsCookieService, ActivityInfrastructureCreator activityInfrastructureCreator, RequestValidator requestValidator, @@ -370,19 +388,15 @@ Ortb2RequestFactory openRtb2RequestFactory( ApplicationSettings applicationSettings, IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, - @Autowired(required = false) UserAdditionalInfoService userAdditionalInfoService, CountryCodeMapper countryCodeMapper, - PriceFloorProcessor priceFloorProcessor, - Metrics metrics, - Clock clock) { + Metrics metrics) { - final List blacklistedAccounts = splitToList(blacklistedAccountsString); + final List blocklistedAccounts = splitToList(blocklistedAccountsString); return new Ortb2RequestFactory( - enforceValidAccount, timeoutAdjustmentFactor, logSamplingRate, - blacklistedAccounts, + blocklistedAccounts, uidsCookieService, activityInfrastructureCreator, requestValidator, @@ -392,11 +406,8 @@ Ortb2RequestFactory openRtb2RequestFactory( applicationSettings, ipAddressHelper, hookStageExecutor, - userAdditionalInfoService, - priceFloorProcessor, countryCodeMapper, - metrics, - clock); + metrics); } @Bean @@ -412,7 +423,8 @@ AuctionRequestFactory auctionRequestFactory( OrtbTypesResolver ortbTypesResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new AuctionRequestFactory( maxRequestSize, @@ -427,7 +439,8 @@ AuctionRequestFactory auctionRequestFactory( ortbTypesResolver, auctionPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -458,7 +471,8 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, FpdResolver fpdResolver, AmpPrivacyContextFactory ampPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new AmpRequestFactory( ortb2RequestFactory, @@ -471,7 +485,8 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, fpdResolver, ampPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -485,7 +500,8 @@ VideoRequestFactory videoRequestFactory( Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new VideoRequestFactory( maxRequestSize, @@ -497,7 +513,8 @@ VideoRequestFactory videoRequestFactory( ortb2ImplicitParametersResolver, auctionPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -508,7 +525,7 @@ VideoResponseFactory videoResponseFactory(JacksonMapper mapper) { @Bean VideoStoredRequestProcessor videoStoredRequestProcessor( @Value("${video.stored-request-required}") boolean enforceStoredRequest, - @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString, + @Value("${auction.blocklisted-accounts}") String blocklistedAccountsString, @Value("${video.stored-requests-timeout-ms}") long defaultTimeoutMs, @Value("${auction.ad-server-currency:#{null}}") String adServerCurrency, @Value("${default-request.file.path:#{null}}") String defaultBidRequestPath, @@ -522,7 +539,7 @@ VideoStoredRequestProcessor videoStoredRequestProcessor( return new VideoStoredRequestProcessor( enforceStoredRequest, - splitToList(blacklistedAccountsString), + splitToList(blocklistedAccountsString), defaultTimeoutMs, adServerCurrency, defaultBidRequestPath, @@ -697,9 +714,8 @@ CookieSyncService cookieSyncService( } @Bean - CookieDeprecationService deprecationCookieResolver(Account defaultAccount) { - - return new CookieDeprecationService(defaultAccount); + CookieDeprecationService cookieDeprecationService() { + return new CookieDeprecationService(); } @Bean @@ -775,7 +791,7 @@ BidderErrorNotifier bidderErrorNotifier( @Bean BidResponseCreator bidResponseCreator( - CacheService cacheService, + CoreCacheService coreCacheService, BidderCatalog bidderCatalog, VastModifier vastModifier, EventsService eventsService, @@ -786,10 +802,12 @@ BidResponseCreator bidResponseCreator( CategoryMappingService categoryMappingService, @Value("${settings.targeting.truncate-attr-chars}") int truncateAttrChars, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl, + @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl) { return new BidResponseCreator( - cacheService, + coreCacheService, bidderCatalog, vastModifier, eventsService, @@ -800,7 +818,8 @@ BidResponseCreator bidResponseCreator( categoryMappingService, truncateAttrChars, clock, - mapper); + mapper, + CacheTtl.of(bannerCacheTtl, videoCacheTtl)); } @Bean @@ -808,9 +827,9 @@ ExchangeService exchangeService( @Value("${logging.sampling-rate:0.01}") double logSamplingRate, BidderCatalog bidderCatalog, StoredResponseProcessor storedResponseProcessor, - @Autowired(required = false) DealsService dealsService, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, CompositeMediaTypeProcessor mediaTypeProcessor, @@ -819,17 +838,13 @@ ExchangeService exchangeService( TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyConversionService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, - @Autowired(required = false) ApplicationEventService applicationEventService, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + PriceFloorProcessor priceFloorProcessor, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -840,9 +855,9 @@ ExchangeService exchangeService( logSamplingRate, bidderCatalog, storedResponseProcessor, - dealsService, privacyEnforcementService, fpdResolver, + impAdjuster, supplyChainResolver, debugResolver, mediaTypeProcessor, @@ -851,23 +866,36 @@ ExchangeService exchangeService( timeoutFactory, bidRequestOrtbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyConversionService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, - applicationEventService, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, - dsaEnforcer, - bidAdjustmentFactorResolver, + priceFloorProcessor, + bidsAdjuster, metrics, clock, mapper, criteriaLogManager, enabledStrictAppSiteDoohValidation); } + @Bean + BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator, + CurrencyConversionService currencyConversionService, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + JacksonMapper mapper) { + + return new BidsAdjuster( + responseBidValidator, + currencyConversionService, + bidAdjustmentFactorResolver, + priceFloorEnforcer, + dsaEnforcer, + mapper); + } + @Bean StoredRequestProcessor storedRequestProcessor( @Value("${auction.stored-requests-timeout-ms}") long defaultTimeoutMs, @@ -982,10 +1010,18 @@ VersionInfo versionInfo(JacksonMapper jacksonMapper) { return VersionInfo.create("git-revision.json", jacksonMapper); } + @Bean + ImpValidator impValidator(BidderParamValidator bidderParamValidator, + BidderCatalog bidderCatalog, + JacksonMapper mapper) { + + return new ImpValidator(bidderParamValidator, bidderCatalog, mapper); + } + @Bean RequestValidator requestValidator( BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, @Value("${logging.sampling-rate:0.01}") double logSamplingRate, @@ -993,7 +1029,7 @@ RequestValidator requestValidator( return new RequestValidator( bidderCatalog, - bidderParamValidator, + impValidator, metrics, mapper, logSamplingRate, @@ -1001,8 +1037,13 @@ RequestValidator requestValidator( } @Bean - PriceFloorsConfigResolver accountValidator(Account defaultAccount, Metrics metrics) { - return new PriceFloorsConfigResolver(defaultAccount, metrics); + PriceFloorsConfigResolver priceFloorsConfigResolver(Metrics metrics) { + return new PriceFloorsConfigResolver(metrics); + } + + @Bean + ActivitiesConfigResolver activitiesConfigResolver(@Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + return new ActivitiesConfigResolver(logSamplingRate); } @Bean @@ -1014,16 +1055,12 @@ BidderParamValidator bidderParamValidator(BidderCatalog bidderCatalog, JacksonMa ResponseBidValidator responseValidator( @Value("${auction.validations.banner-creative-max-size}") BidValidationEnforcement bannerMaxSizeEnforcement, @Value("${auction.validations.secure-markup}") BidValidationEnforcement secureMarkupEnforcement, - Metrics metrics, - JacksonMapper mapper, - @Value("${deals.enabled}") boolean dealsEnabled) { + Metrics metrics) { return new ResponseBidValidator( bannerMaxSizeEnforcement, secureMarkupEnforcement, metrics, - mapper, - dealsEnabled, logSamplingRate); } @@ -1124,22 +1161,15 @@ LoggerControlKnob loggerControlKnob(Vertx vertx) { } @Bean - DsaEnforcer dsaEnforcer() { - return new DsaEnforcer(); + DsaEnforcer dsaEnforcer(JacksonMapper mapper) { + return new DsaEnforcer(mapper); } @Bean - Account defaultAccount(@Value("${settings.default-account-config:#{null}}") String defaultAccountConfig, - JacksonMapper mapper) { - try { - final Account account = StringUtils.isNotBlank(defaultAccountConfig) - ? mapper.decodeValue(defaultAccountConfig, Account.class) - : null; - return account != null ? account : Account.builder().build(); - } catch (DecodeException e) { - logger.warn("Could not parse default account configuration", e); - return Account.builder().build(); - } + SkippedAuctionService skipAuctionService(StoredResponseProcessor storedResponseProcessor, + BidResponseCreator bidResponseCreator) { + + return new SkippedAuctionService(storedResponseProcessor, bidResponseCreator); } private static List splitToList(String listAsString) { diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 25edba67976..f7aaa9bb4ba 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.UtilityClass; import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JacksonMapper; @@ -15,17 +16,19 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.CachingApplicationSettings; import org.prebid.server.settings.CompositeApplicationSettings; +import org.prebid.server.settings.DatabaseApplicationSettings; import org.prebid.server.settings.EnrichingApplicationSettings; import org.prebid.server.settings.FileApplicationSettings; import org.prebid.server.settings.HttpApplicationSettings; -import org.prebid.server.settings.JdbcApplicationSettings; +import org.prebid.server.settings.S3ApplicationSettings; import org.prebid.server.settings.SettingsCache; -import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.helper.ParametrizedQueryHelper; +import org.prebid.server.settings.service.DatabasePeriodicRefreshService; import org.prebid.server.settings.service.HttpPeriodicRefreshService; -import org.prebid.server.settings.service.JdbcPeriodicRefreshService; +import org.prebid.server.settings.service.S3PeriodicRefreshService; import org.prebid.server.spring.config.database.DatabaseConfiguration; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.jdbc.JdbcClient; +import org.prebid.server.vertx.database.DatabaseClient; +import org.prebid.server.vertx.httpclient.HttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -36,12 +39,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Clock; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; @UtilityClass @@ -72,17 +83,19 @@ FileApplicationSettings fileApplicationSettings( static class DatabaseSettingsConfiguration { @Bean - JdbcApplicationSettings jdbcApplicationSettings( + DatabaseApplicationSettings databaseApplicationSettings( @Value("${settings.database.account-query}") String accountQuery, @Value("${settings.database.stored-requests-query}") String storedRequestsQuery, @Value("${settings.database.amp-stored-requests-query}") String ampStoredRequestsQuery, @Value("${settings.database.stored-responses-query}") String storedResponsesQuery, - JdbcClient jdbcClient, + ParametrizedQueryHelper parametrizedQueryHelper, + DatabaseClient databaseClient, JacksonMapper jacksonMapper) { - return new JdbcApplicationSettings( - jdbcClient, + return new DatabaseApplicationSettings( + databaseClient, jacksonMapper, + parametrizedQueryHelper, accountQuery, storedRequestsQuery, ampStoredRequestsQuery, @@ -148,21 +161,21 @@ public HttpPeriodicRefreshService ampHttpPeriodicRefreshService( @Configuration @ConditionalOnProperty( - prefix = "settings.in-memory-cache.jdbc-update", + prefix = "settings.in-memory-cache.database-update", name = {"refresh-rate", "timeout", "init-query", "update-query", "amp-init-query", "amp-update-query"}) - static class JdbcPeriodicRefreshServiceConfiguration { + static class DatabasePeriodicRefreshServiceConfiguration { - @Value("${settings.in-memory-cache.jdbc-update.refresh-rate}") + @Value("${settings.in-memory-cache.database-update.refresh-rate}") long refreshPeriod; - @Value("${settings.in-memory-cache.jdbc-update.timeout}") + @Value("${settings.in-memory-cache.database-update.timeout}") long timeout; @Autowired Vertx vertx; @Autowired - JdbcClient jdbcClient; + DatabaseClient databaseClient; @Autowired TimeoutFactory timeoutFactory; @@ -174,12 +187,12 @@ static class JdbcPeriodicRefreshServiceConfiguration { Clock clock; @Bean - public JdbcPeriodicRefreshService jdbcPeriodicRefreshService( + public DatabasePeriodicRefreshService databasePeriodicRefreshService( @Qualifier("settingsCache") SettingsCache settingsCache, - @Value("${settings.in-memory-cache.jdbc-update.init-query}") String initQuery, - @Value("${settings.in-memory-cache.jdbc-update.update-query}") String updateQuery) { + @Value("${settings.in-memory-cache.database-update.init-query}") String initQuery, + @Value("${settings.in-memory-cache.database-update.update-query}") String updateQuery) { - return new JdbcPeriodicRefreshService( + return new DatabasePeriodicRefreshService( initQuery, updateQuery, refreshPeriod, @@ -187,19 +200,19 @@ public JdbcPeriodicRefreshService jdbcPeriodicRefreshService( MetricName.stored_request, settingsCache, vertx, - jdbcClient, + databaseClient, timeoutFactory, metrics, clock); } @Bean - public JdbcPeriodicRefreshService ampJdbcPeriodicRefreshService( + public DatabasePeriodicRefreshService ampDatabasePeriodicRefreshService( @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache, - @Value("${settings.in-memory-cache.jdbc-update.amp-init-query}") String ampInitQuery, - @Value("${settings.in-memory-cache.jdbc-update.amp-update-query}") String ampUpdateQuery) { + @Value("${settings.in-memory-cache.database-update.amp-init-query}") String ampInitQuery, + @Value("${settings.in-memory-cache.database-update.amp-update-query}") String ampUpdateQuery) { - return new JdbcPeriodicRefreshService( + return new DatabasePeriodicRefreshService( ampInitQuery, ampUpdateQuery, refreshPeriod, @@ -207,13 +220,122 @@ public JdbcPeriodicRefreshService ampJdbcPeriodicRefreshService( MetricName.amp_stored_request, ampSettingsCache, vertx, - jdbcClient, + databaseClient, timeoutFactory, metrics, clock); } } + @Configuration + @ConditionalOnProperty(prefix = "settings.s3", name = {"accounts-dir", "stored-imps-dir", "stored-requests-dir"}) + static class S3SettingsConfiguration { + + @Component + @ConfigurationProperties(prefix = "settings.s3") + @ConditionalOnProperty(prefix = "settings.s3", name = {"accessKeyId", "secretAccessKey"}) + @Validated + @Data + @NoArgsConstructor + protected static class S3ConfigurationProperties { + + @NotBlank + private String accessKeyId; + + @NotBlank + private String secretAccessKey; + + /** + * If not provided AWS_GLOBAL will be used as a region + */ + private String region; + + @NotBlank + private String endpoint; + + @NotBlank + private String bucket; + + @NotBlank + private Boolean forcePathStyle; + + @NotBlank + private String accountsDir; + + @NotBlank + private String storedImpsDir; + + @NotBlank + private String storedRequestsDir; + + @NotBlank + private String storedResponsesDir; + } + + @Bean + S3AsyncClient s3AsyncClient(S3ConfigurationProperties s3ConfigurationProperties) throws URISyntaxException { + final AwsBasicCredentials credentials = AwsBasicCredentials.create( + s3ConfigurationProperties.getAccessKeyId(), + s3ConfigurationProperties.getSecretAccessKey()); + final Region awsRegion = Optional.ofNullable(s3ConfigurationProperties.getRegion()) + .map(Region::of) + .orElse(Region.AWS_GLOBAL); + + return S3AsyncClient + .builder() + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .endpointOverride(new URI(s3ConfigurationProperties.getEndpoint())) + .forcePathStyle(s3ConfigurationProperties.getForcePathStyle()) + .region(awsRegion) + .build(); + } + + @Bean + S3ApplicationSettings s3ApplicationSettings(S3AsyncClient s3AsyncClient, + S3ConfigurationProperties s3ConfigurationProperties, + JacksonMapper mapper, + Vertx vertx) { + + return new S3ApplicationSettings( + s3AsyncClient, + s3ConfigurationProperties.getBucket(), + s3ConfigurationProperties.getAccountsDir(), + s3ConfigurationProperties.getStoredImpsDir(), + s3ConfigurationProperties.getStoredRequestsDir(), + s3ConfigurationProperties.getStoredResponsesDir(), + mapper, + vertx); + } + } + + @Configuration + @ConditionalOnProperty(prefix = "settings.in-memory-cache.s3-update", name = {"refresh-rate", "timeout"}) + static class S3PeriodicRefreshServiceConfiguration { + + @Bean + public S3PeriodicRefreshService s3PeriodicRefreshService( + S3AsyncClient s3AsyncClient, + S3SettingsConfiguration.S3ConfigurationProperties s3ConfigurationProperties, + @Value("${settings.in-memory-cache.s3-update.refresh-rate}") long refreshPeriod, + SettingsCache settingsCache, + Clock clock, + Metrics metrics, + Vertx vertx) { + + return new S3PeriodicRefreshService( + s3AsyncClient, + s3ConfigurationProperties.getBucket(), + s3ConfigurationProperties.getStoredRequestsDir(), + s3ConfigurationProperties.getStoredImpsDir(), + refreshPeriod, + settingsCache, + MetricName.stored_request, + clock, + metrics, + vertx); + } + } + /** * This configuration defines a collection of application settings fetchers and its ordering. */ @@ -223,15 +345,17 @@ static class CompositeSettingsConfiguration { @Bean CompositeApplicationSettings compositeApplicationSettings( @Autowired(required = false) FileApplicationSettings fileApplicationSettings, - @Autowired(required = false) JdbcApplicationSettings jdbcApplicationSettings, - @Autowired(required = false) HttpApplicationSettings httpApplicationSettings) { - - final List applicationSettingsList = - Stream.of(fileApplicationSettings, - jdbcApplicationSettings, - httpApplicationSettings) - .filter(Objects::nonNull) - .toList(); + @Autowired(required = false) DatabaseApplicationSettings databaseApplicationSettings, + @Autowired(required = false) HttpApplicationSettings httpApplicationSettings, + @Autowired(required = false) S3ApplicationSettings s3ApplicationSettings) { + + final List applicationSettingsList = Stream.of( + fileApplicationSettings, + databaseApplicationSettings, + s3ApplicationSettings, + httpApplicationSettings) + .filter(Objects::nonNull) + .toList(); return new CompositeApplicationSettings(applicationSettingsList); } @@ -243,19 +367,21 @@ static class EnrichingSettingsConfiguration { @Bean EnrichingApplicationSettings enrichingApplicationSettings( @Value("${settings.enforce-valid-account}") boolean enforceValidAccount, - @Value("${logging.sampling-rate:0.01}") double logSamplingRate, - Account defaultAccount, + @Value("${settings.default-account-config:#{null}}") String defaultAccountConfig, + JacksonMapper mapper, CompositeApplicationSettings compositeApplicationSettings, PriceFloorsConfigResolver priceFloorsConfigResolver, + ActivitiesConfigResolver activitiesConfigResolver, JsonMerger jsonMerger) { return new EnrichingApplicationSettings( enforceValidAccount, - logSamplingRate, - defaultAccount, + defaultAccountConfig, compositeApplicationSettings, priceFloorsConfigResolver, - jsonMerger); + activitiesConfigResolver, + jsonMerger, + mapper); } } @@ -279,7 +405,8 @@ CachingApplicationSettings cachingApplicationSettings( videoCache, metrics, cacheProperties.getTtlSeconds(), - cacheProperties.getCacheSize()); + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } } @@ -301,19 +428,28 @@ static class CacheConfiguration { @Bean @Qualifier("settingsCache") SettingsCache settingsCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache(cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize()); + return new SettingsCache( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } @Bean @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache(cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize()); + return new SettingsCache( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } @Bean @Qualifier("videoSettingCache") SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache(cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize()); + return new SettingsCache( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } } @@ -323,7 +459,7 @@ SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperti @Validated @Data @NoArgsConstructor - private static class ApplicationSettingsCacheProperties { + protected static class ApplicationSettingsCacheProperties { @NotNull @Min(1) @@ -331,5 +467,7 @@ private static class ApplicationSettingsCacheProperties { @NotNull @Min(1) private Integer cacheSize; + @Min(0) + private int jitterSeconds; } } diff --git a/src/main/java/org/prebid/server/spring/config/VerticleStarter.java b/src/main/java/org/prebid/server/spring/config/VerticleStarter.java new file mode 100644 index 00000000000..cb519b88477 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/VerticleStarter.java @@ -0,0 +1,40 @@ +package org.prebid.server.spring.config; + +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Vertx; +import org.prebid.server.vertx.ContextRunner; +import org.prebid.server.vertx.verticles.VerticleDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; + +import java.util.List; + +@Configuration +public class VerticleStarter { + + @Autowired + private Vertx vertx; + + @Autowired + private ContextRunner contextRunner; + + @Autowired + private List definitions; + + @EventListener(ContextRefreshedEvent.class) + public void start() { + for (VerticleDefinition definition : definitions) { + if (definition.getAmount() <= 0) { + continue; + } + + contextRunner.runBlocking(promise -> + vertx.deployVerticle( + definition.getFactory(), + new DeploymentOptions().setInstances(definition.getAmount()), + promise)); + } + } +} diff --git a/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java b/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java index 4636a056783..9b5250a4233 100644 --- a/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java @@ -2,17 +2,15 @@ import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; -import io.vertx.core.eventbus.EventBus; import io.vertx.core.file.FileSystem; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.dropwizard.DropwizardMetricsOptions; import io.vertx.ext.dropwizard.Match; import io.vertx.ext.dropwizard.MatchType; import io.vertx.ext.web.handler.BodyHandler; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.spring.config.metrics.MetricsConfiguration; import org.prebid.server.vertx.ContextRunner; -import org.prebid.server.vertx.LocalMessageCodec; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,18 +38,10 @@ Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize, .setMetricsOptions(metricsOptions); final Vertx vertx = Vertx.vertx(vertxOptions); - logger.info("Native transport enabled: {0}", vertx.isNativeTransportEnabled()); + logger.info("Native transport enabled: {}", vertx.isNativeTransportEnabled()); return vertx; } - @Bean - EventBus eventBus(Vertx vertx) { - final EventBus eventBus = vertx.eventBus(); - eventBus.registerCodec(LocalMessageCodec.create()); - - return eventBus; - } - @Bean FileSystem fileSystem(Vertx vertx) { return vertx.fileSystem(); diff --git a/src/main/java/org/prebid/server/spring/config/VertxContextScope.java b/src/main/java/org/prebid/server/spring/config/VertxContextScope.java index b1d79b1a0aa..8dbc6869695 100644 --- a/src/main/java/org/prebid/server/spring/config/VertxContextScope.java +++ b/src/main/java/org/prebid/server/spring/config/VertxContextScope.java @@ -1,8 +1,8 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.springframework.beans.factory.ObjectFactory; import org.springframework.context.support.SimpleThreadScope; diff --git a/src/main/java/org/prebid/server/spring/config/WebConfiguration.java b/src/main/java/org/prebid/server/spring/config/WebConfiguration.java deleted file mode 100644 index c6244306b97..00000000000 --- a/src/main/java/org/prebid/server/spring/config/WebConfiguration.java +++ /dev/null @@ -1,446 +0,0 @@ -package org.prebid.server.spring.config; - -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.JksOptions; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.handler.CorsHandler; -import io.vertx.ext.web.handler.StaticHandler; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; -import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; -import org.prebid.server.auction.AmpResponsePostProcessor; -import org.prebid.server.auction.ExchangeService; -import org.prebid.server.auction.VideoResponseFactory; -import org.prebid.server.auction.gpp.CookieSyncGppService; -import org.prebid.server.auction.gpp.SetuidGppService; -import org.prebid.server.auction.privacy.contextfactory.CookieSyncPrivacyContextFactory; -import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory; -import org.prebid.server.auction.requestfactory.AmpRequestFactory; -import org.prebid.server.auction.requestfactory.AuctionRequestFactory; -import org.prebid.server.auction.requestfactory.VideoRequestFactory; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.cache.CacheService; -import org.prebid.server.cookie.CookieDeprecationService; -import org.prebid.server.cookie.CookieSyncService; -import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.deals.UserService; -import org.prebid.server.deals.events.ApplicationEventService; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.handler.BidderParamHandler; -import org.prebid.server.handler.CookieSyncHandler; -import org.prebid.server.handler.CustomizedAdminEndpoint; -import org.prebid.server.handler.ExceptionHandler; -import org.prebid.server.handler.GetuidsHandler; -import org.prebid.server.handler.NoCacheHandler; -import org.prebid.server.handler.NotificationEventHandler; -import org.prebid.server.handler.OptoutHandler; -import org.prebid.server.handler.SetuidHandler; -import org.prebid.server.handler.StatusHandler; -import org.prebid.server.handler.VtrackHandler; -import org.prebid.server.handler.info.BidderDetailsHandler; -import org.prebid.server.handler.info.BiddersHandler; -import org.prebid.server.handler.info.filters.BaseOnlyBidderInfoFilterStrategy; -import org.prebid.server.handler.info.filters.BidderInfoFilterStrategy; -import org.prebid.server.handler.info.filters.EnabledOnlyBidderInfoFilterStrategy; -import org.prebid.server.handler.openrtb2.AmpHandler; -import org.prebid.server.handler.openrtb2.VideoHandler; -import org.prebid.server.health.HealthChecker; -import org.prebid.server.health.PeriodicHealthChecker; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.HttpInteractionLogger; -import org.prebid.server.metric.Metrics; -import org.prebid.server.optout.GoogleRecaptchaVerifier; -import org.prebid.server.privacy.HostVendorTcfDefinerService; -import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.validation.BidderParamValidator; -import org.prebid.server.version.PrebidVersionProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; - -import java.time.Clock; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -@Configuration -public class WebConfiguration { - - @Value("${logging.sampling-rate:0.01}") - private double logSamplingRate; - - @Autowired - private Vertx vertx; - - // TODO: remove support for properties with http prefix after transition period - @Bean - HttpServerOptions httpServerOptions( - @Value("#{'${http.max-headers-size:${server.max-headers-size:}}'}") int maxHeaderSize, - @Value("#{'${http.max-initial-line-length:${server.max-initial-line-length:}}'}") int maxInitialLineLength, - @Value("#{'${http.ssl:${server.ssl:}}'}") boolean ssl, - @Value("#{'${http.jks-path:${server.jks-path:}}'}") String jksPath, - @Value("#{'${http.jks-password:${server.jks-password:}}'}") String jksPassword, - @Value("#{'${http.idle-timeout:${server.idle-timeout}}'}") int idleTimeout, - @Value("${server.enable-quickack:#{null}}") Optional enableQuickAck, - @Value("${server.enable-reuseport:#{null}}") Optional enableReusePort) { - - final HttpServerOptions httpServerOptions = new HttpServerOptions() - .setHandle100ContinueAutomatically(true) - .setMaxInitialLineLength(maxInitialLineLength) - .setMaxHeaderSize(maxHeaderSize) - .setCompressionSupported(true) - .setDecompressionSupported(true) - .setIdleTimeout(idleTimeout); // kick off long processing requests, value in seconds - enableQuickAck.ifPresent(httpServerOptions::setTcpQuickAck); - enableReusePort.ifPresent(httpServerOptions::setReusePort); - if (ssl) { - final JksOptions jksOptions = new JksOptions() - .setPath(jksPath) - .setPassword(jksPassword); - - httpServerOptions - .setSsl(true) - .setKeyStoreOptions(jksOptions); - } - - return httpServerOptions; - } - - @Bean - ExceptionHandler exceptionHandler(Metrics metrics) { - return ExceptionHandler.create(metrics); - } - - @Bean("router") - Router router(BodyHandler bodyHandler, - NoCacheHandler noCacheHandler, - CorsHandler corsHandler, - org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler, - AmpHandler openrtbAmpHandler, - VideoHandler openrtbVideoHandler, - StatusHandler statusHandler, - CookieSyncHandler cookieSyncHandler, - SetuidHandler setuidHandler, - GetuidsHandler getuidsHandler, - VtrackHandler vtrackHandler, - OptoutHandler optoutHandler, - BidderParamHandler bidderParamHandler, - BiddersHandler biddersHandler, - BidderDetailsHandler bidderDetailsHandler, - NotificationEventHandler notificationEventHandler, - List customizedAdminEndpoints, - StaticHandler staticHandler) { - - final Router router = Router.router(vertx); - router.route().handler(bodyHandler); - router.route().handler(noCacheHandler); - router.route().handler(corsHandler); - router.post("/openrtb2/auction").handler(openrtbAuctionHandler); - router.get("/openrtb2/amp").handler(openrtbAmpHandler); - router.post("/openrtb2/video").handler(openrtbVideoHandler); - router.get("/status").handler(statusHandler); - router.post("/cookie_sync").handler(cookieSyncHandler); - router.get("/setuid").handler(setuidHandler); - router.get("/getuids").handler(getuidsHandler); - router.post("/vtrack").handler(vtrackHandler); - router.post("/optout").handler(optoutHandler); - router.get("/optout").handler(optoutHandler); - router.get("/bidders/params").handler(bidderParamHandler); - router.get("/info/bidders").handler(biddersHandler); - router.get("/info/bidders/:bidderName").handler(bidderDetailsHandler); - router.get("/event").handler(notificationEventHandler); - - customizedAdminEndpoints.stream() - .filter(CustomizedAdminEndpoint::isOnApplicationPort) - .forEach(customizedAdminEndpoint -> customizedAdminEndpoint.router(router)); - - router.get("/static/*").handler(staticHandler); - router.get("/").handler(staticHandler); // serves index.html by default - - return router; - } - - @Bean - NoCacheHandler noCacheHandler() { - return NoCacheHandler.create(); - } - - @Bean - CorsHandler corsHandler() { - return CorsHandler.create(".*") - .allowCredentials(true) - .allowedHeaders(new HashSet<>(Arrays.asList( - HttpUtil.ORIGIN_HEADER.toString(), - HttpUtil.ACCEPT_HEADER.toString(), - HttpUtil.CONTENT_TYPE_HEADER.toString(), - HttpUtil.X_REQUESTED_WITH_HEADER.toString()))) - .allowedMethods(new HashSet<>(Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.HEAD, - HttpMethod.OPTIONS))); - } - - @Bean - org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler( - ExchangeService exchangeService, - AuctionRequestFactory auctionRequestFactory, - AnalyticsReporterDelegator analyticsReporter, - Metrics metrics, - Clock clock, - HttpInteractionLogger httpInteractionLogger, - PrebidVersionProvider prebidVersionProvider, - JacksonMapper mapper) { - - return new org.prebid.server.handler.openrtb2.AuctionHandler( - logSamplingRate, - auctionRequestFactory, - exchangeService, - analyticsReporter, - metrics, - clock, - httpInteractionLogger, - prebidVersionProvider, - mapper); - } - - @Bean - AmpHandler openrtbAmpHandler( - AmpRequestFactory ampRequestFactory, - ExchangeService exchangeService, - AnalyticsReporterDelegator analyticsReporter, - Metrics metrics, - Clock clock, - BidderCatalog bidderCatalog, - AmpProperties ampProperties, - AmpResponsePostProcessor ampResponsePostProcessor, - HttpInteractionLogger httpInteractionLogger, - PrebidVersionProvider prebidVersionProvider, - JacksonMapper mapper) { - - return new AmpHandler( - ampRequestFactory, - exchangeService, - analyticsReporter, - metrics, - clock, - bidderCatalog, - ampProperties.getCustomTargetingSet(), - ampResponsePostProcessor, - httpInteractionLogger, - prebidVersionProvider, - mapper, - logSamplingRate); - } - - @Bean - VideoHandler openrtbVideoHandler( - VideoRequestFactory videoRequestFactory, - VideoResponseFactory videoResponseFactory, - ExchangeService exchangeService, - CacheService cacheService, - AnalyticsReporterDelegator analyticsReporter, - Metrics metrics, - Clock clock, - PrebidVersionProvider prebidVersionProvider, - JacksonMapper mapper) { - - return new VideoHandler( - videoRequestFactory, - videoResponseFactory, - exchangeService, - cacheService, analyticsReporter, - metrics, - clock, - prebidVersionProvider, - mapper); - } - - @Bean - StatusHandler statusHandler(List healthCheckers, JacksonMapper mapper) { - healthCheckers.stream() - .filter(PeriodicHealthChecker.class::isInstance) - .map(PeriodicHealthChecker.class::cast) - .forEach(PeriodicHealthChecker::initialize); - return new StatusHandler(healthCheckers, mapper); - } - - @Bean - CookieSyncHandler cookieSyncHandler( - @Value("${cookie-sync.default-timeout-ms}") int defaultTimeoutMs, - UidsCookieService uidsCookieService, - CookieSyncGppService cookieSyncGppProcessor, - CookieDeprecationService cookieDeprecationService, - ActivityInfrastructureCreator activityInfrastructureCreator, - ApplicationSettings applicationSettings, - CookieSyncService cookieSyncService, - CookieSyncPrivacyContextFactory cookieSyncPrivacyContextFactory, - AnalyticsReporterDelegator analyticsReporterDelegator, - Metrics metrics, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { - - return new CookieSyncHandler( - defaultTimeoutMs, - logSamplingRate, - uidsCookieService, - cookieDeprecationService, - cookieSyncGppProcessor, - activityInfrastructureCreator, - cookieSyncService, - applicationSettings, - cookieSyncPrivacyContextFactory, - analyticsReporterDelegator, - metrics, - timeoutFactory, - mapper); - } - - @Bean - SetuidHandler setuidHandler( - @Value("${setuid.default-timeout-ms}") int defaultTimeoutMs, - UidsCookieService uidsCookieService, - ApplicationSettings applicationSettings, - BidderCatalog bidderCatalog, - SetuidPrivacyContextFactory setuidPrivacyContextFactory, - SetuidGppService setuidGppService, - ActivityInfrastructureCreator activityInfrastructureCreator, - HostVendorTcfDefinerService tcfDefinerService, - AnalyticsReporterDelegator analyticsReporter, - Metrics metrics, - TimeoutFactory timeoutFactory) { - - return new SetuidHandler( - defaultTimeoutMs, - uidsCookieService, - applicationSettings, - bidderCatalog, - setuidPrivacyContextFactory, - setuidGppService, - activityInfrastructureCreator, - tcfDefinerService, - analyticsReporter, - metrics, - timeoutFactory); - } - - @Bean - GetuidsHandler getuidsHandler(UidsCookieService uidsCookieService, JacksonMapper mapper) { - return new GetuidsHandler(uidsCookieService, mapper); - } - - @Bean - VtrackHandler vtrackHandler( - @Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs, - @Value("${vtrack.allow-unknown-bidder}") boolean allowUnknownBidder, - @Value("${vtrack.modify-vast-for-unknown-bidder}") boolean modifyVastForUnknownBidder, - ApplicationSettings applicationSettings, - BidderCatalog bidderCatalog, - CacheService cacheService, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { - - return new VtrackHandler( - defaultTimeoutMs, - allowUnknownBidder, - modifyVastForUnknownBidder, - applicationSettings, - bidderCatalog, - cacheService, - timeoutFactory, - mapper); - } - - @Bean - OptoutHandler optoutHandler( - @Value("${external-url}") String externalUrl, - @Value("${host-cookie.opt-out-url}") String optoutUrl, - @Value("${host-cookie.opt-in-url}") String optinUrl, - GoogleRecaptchaVerifier googleRecaptchaVerifier, - UidsCookieService uidsCookieService) { - - return new OptoutHandler( - googleRecaptchaVerifier, - uidsCookieService, - OptoutHandler.getOptoutRedirectUrl(externalUrl), - HttpUtil.validateUrl(optoutUrl), - HttpUtil.validateUrl(optinUrl)); - } - - @Bean - BidderParamHandler bidderParamHandler(BidderParamValidator bidderParamValidator) { - return new BidderParamHandler(bidderParamValidator); - } - - @Bean - BidderInfoFilterStrategy enabledOnlyBidderInfoFilterStrategy(BidderCatalog bidderCatalog) { - return new EnabledOnlyBidderInfoFilterStrategy(bidderCatalog); - } - - @Bean - BidderInfoFilterStrategy baseOnlyBidderInfoFilterStrategy(BidderCatalog bidderCatalog) { - return new BaseOnlyBidderInfoFilterStrategy(bidderCatalog); - } - - @Bean - BiddersHandler biddersHandler(BidderCatalog bidderCatalog, - List filterStrategies, - JacksonMapper mapper) { - return new BiddersHandler(bidderCatalog, filterStrategies, mapper); - } - - @Bean - BidderDetailsHandler bidderDetailsHandler(BidderCatalog bidderCatalog, JacksonMapper mapper) { - return new BidderDetailsHandler(bidderCatalog, mapper); - } - - @Bean - NotificationEventHandler notificationEventHandler( - UidsCookieService uidsCookieService, - @Autowired(required = false) ApplicationEventService applicationEventService, - @Autowired(required = false) UserService userService, - ActivityInfrastructureCreator activityInfrastructureCreator, - AnalyticsReporterDelegator analyticsReporterDelegator, - TimeoutFactory timeoutFactory, - ApplicationSettings applicationSettings, - @Value("${event.default-timeout-ms}") long defaultTimeoutMillis, - @Value("${deals.enabled}") boolean dealsEnabled) { - - return new NotificationEventHandler( - uidsCookieService, - applicationEventService, - userService, - activityInfrastructureCreator, - analyticsReporterDelegator, - timeoutFactory, - applicationSettings, - defaultTimeoutMillis, - dealsEnabled); - } - - @Bean - StaticHandler staticHandler() { - return StaticHandler.create("static").setCachingEnabled(false); - } - - @Component - @ConfigurationProperties(prefix = "amp") - @Data - @NoArgsConstructor - private static class AmpProperties { - - private List customTargeting = new ArrayList<>(); - - Set getCustomTargetingSet() { - return new HashSet<>(customTargeting); - } - } -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java index 3e6e38cee29..83cc2694122 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/aax.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AceexConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AceexConfiguration.java index d93b5428a19..f8a168d86b8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AceexConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AceexConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/aceex.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java index 14aeb9c023d..77bec41b1c2 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/acuityads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdQueryConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdQueryConfiguration.java index 8c68cc8422c..0f5dda54587 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdQueryConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdQueryConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adquery.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdelementConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdelementConfiguration.java index d3d1a0e0e7a..c38ca1c1fbe 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdelementConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdelementConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adelement.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java index 43d1f0fd4f6..64091a95ef0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adf.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java index 3d551f7fe34..b377b46d1f2 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adgeneration.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java index 33cb3fbf6a7..d0ac8674d9a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adhese.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java index 23974f5a2ff..ca4ff6cbd07 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adkerneladn.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java index 3defba5f55c..cc360ff91f8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adkernel.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java index 0be512c5930..c4b5f011268 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adman.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdmaticConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdmaticConfiguration.java new file mode 100644 index 00000000000..49237835dc2 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdmaticConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.admatic.AdmaticBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/admatic.yaml", factory = YamlPropertySourceFactory.class) +public class AdmaticConfiguration { + + private static final String BIDDER_NAME = "admatic"; + + @Bean("admaticConfigurationProperties") + @ConfigurationProperties("adapters.admatic") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps admaticBidderDeps(BidderConfigurationProperties admaticConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(admaticConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdmaticBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java index f981511bd4b..2a7a4cc8596 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/admixer.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java index d31a23ade02..687c2897b8f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.time.Clock; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java index 2d41d75e914..d16984e3c5e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adocean.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java index aabbf780e6a..89163bceeab 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adoppler.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java index 8c7eb644316..9c0ac457d71 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adot.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java index 2a41025ce3e..3792a998ea8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adpone.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java index 4792b91ec5b..c9f6f950059 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adprime.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdrinoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdrinoConfiguration.java deleted file mode 100644 index 601b108e571..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdrinoConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.adrino.AdrinoBidder; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; -import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/adrino.yaml", factory = YamlPropertySourceFactory.class) -public class AdrinoConfiguration { - - private static final String BIDDER_NAME = "adrino"; - - @Bean("adrinoConfigurationProperties") - @ConfigurationProperties("adapters.adrino") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps adrinoBidderDeps(BidderConfigurationProperties adrinoConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(adrinoConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new AdrinoBidder(config.getEndpoint(), mapper)) - .assemble(); - } -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java index 04d87dbb076..2b3996f7fef 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adtarget.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java index b45262b6a70..de3713761b0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adtelligent.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java new file mode 100644 index 00000000000..8a86c88ac81 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adtonos.AdtonosBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/adtonos.yaml", factory = YamlPropertySourceFactory.class) +public class AdtonosConfiguration { + + private static final String BIDDER_NAME = "adtonos"; + + @Bean("adtonosConfigurationProperties") + @ConfigurationProperties("adapters.adtonos") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adtonosBidderDeps(BidderConfigurationProperties adtonosConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adtonosConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdtonosBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtrgtmeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtrgtmeConfiguration.java index b544af026a6..7a578330380 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdtrgtmeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtrgtmeConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adtrgtme.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java index a5b9b8b2278..7b58430ee4c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/advangelists.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java index 40b81bf434f..58b17ef619f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adview.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java index 197ca9b4d0a..6e6c4659e36 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adxcg.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java index 37f41f487d3..d1350a9f53a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/adyoulike.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AidemConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AidemConfiguration.java index 3e61efb79eb..7689a338302 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AidemConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AidemConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/aidem.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java index 76894a12476..216aa69047e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/aja.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java index 57f7395f478..1082e4c0b5a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/algorix.yaml", diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AlkimiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AlkimiConfiguration.java index 70bb071c850..44f8e0d50d8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AlkimiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AlkimiConfiguration.java @@ -7,15 +7,13 @@ import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/alkimi.yaml", factory = YamlPropertySourceFactory.class) @@ -23,17 +21,6 @@ public class AlkimiConfiguration { private static final String BIDDER_NAME = "alkimi"; - @Value("${external-url}") - @NotBlank - private String externalUrl; - - @Autowired - private JacksonMapper mapper; - - @Autowired - @Qualifier("alkimiConfigurationProperties") - private BidderConfigurationProperties configProperties; - @Bean("alkimiConfigurationProperties") @ConfigurationProperties("adapters.alkimi") BidderConfigurationProperties configurationProperties() { @@ -41,9 +28,12 @@ BidderConfigurationProperties configurationProperties() { } @Bean - BidderDeps alkimiBidderDeps() { + BidderDeps alkimiBidderDeps(BidderConfigurationProperties alkimiConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(configProperties) + .withConfig(alkimiConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) .bidderCreator(config -> new AlkimiBidder(config.getEndpoint(), mapper)) .assemble(); diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java index efdc07c829d..d90cafd1fa8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/amx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ApacdexConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ApacdexConfiguration.java index c65595c6055..1d8c5882416 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ApacdexConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ApacdexConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/apacdex.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java index 17d293b98bf..87ca781f346 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java @@ -16,7 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.Map; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AppushConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AppushConfiguration.java index 977fd84a330..af0c97490cd 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AppushConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AppushConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/appush.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AsoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AsoConfiguration.java new file mode 100644 index 00000000000..840b578ea28 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AsoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.aso.AsoBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/aso.yaml", factory = YamlPropertySourceFactory.class) +public class AsoConfiguration { + + private static final String BIDDER_NAME = "aso"; + + @Bean("asoConfigurationProperties") + @ConfigurationProperties("adapters.aso") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps asoBidderDeps(BidderConfigurationProperties asoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(asoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AsoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AudienceNetworkConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AudienceNetworkConfiguration.java index 3e984b575e3..49b0d3ff049 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AudienceNetworkConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AudienceNetworkConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.util.function.Function; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AutomatadBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AutomatadBidderConfiguration.java index 95d564260dc..18f2e1a62bc 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AutomatadBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AutomatadBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/automatad.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java index 722e2f13d0b..87bb0b24f1a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/avocet.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AxisConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AxisConfiguration.java index 292df8b9edb..93b896371af 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AxisConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AxisConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/axis.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java index 69643f1bb45..8423eb76cf2 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/axonix.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java index 71b24f5861a..e34cf171976 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/beachfront.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java index b9d44061493..b7113c8bbcc 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/beintoo.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BematterfullConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BematterfullConfiguration.java index 9de51f3d6cc..bef7077d93c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BematterfullConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BematterfullConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bematterfull.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java index ab7fdeab33b..448104e62ba 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/between.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BeyondMediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BeyondMediaConfiguration.java index cf2c0729d52..541a70fa294 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BeyondMediaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BeyondMediaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/beyondmedia.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java index 8eed33412ae..d5171cff6fb 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bidmachine.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java new file mode 100644 index 00000000000..c5622397e71 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bidmatic.BidmaticBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/bidmatic.yaml", factory = YamlPropertySourceFactory.class) +public class BidmaticConfiguration { + + private static final String BIDDER_NAME = "bidmatic"; + + @Bean("bidmaticConfigurationProperties") + @ConfigurationProperties("adapters.bidmatic") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bidmaticBidderDeps(BidderConfigurationProperties bidmaticConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bidmaticConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BidmaticBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java index bcdcbe438e9..ebd9305906f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bidmyadz.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java index 235c24cc161..389200d09c0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bidscube.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidstackConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidstackConfiguration.java index af6f47d68b9..1103ac66853 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BidstackConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidstackConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bidstack.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BigoadConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BigoadConfiguration.java new file mode 100644 index 00000000000..bd3c932efc2 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BigoadConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bigoad.BigoadBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/bigoad.yaml", factory = YamlPropertySourceFactory.class) +public class BigoadConfiguration { + + private static final String BIDDER_NAME = "bigoad"; + + @Bean("bigoadConfigurationProperties") + @ConfigurationProperties("adapters.bigoad") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bigoadBidderDeps(BidderConfigurationProperties bigoadConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bigoadConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BigoadBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java deleted file mode 100644 index 90b8491f55e..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.bizzclick.BizzclickBidder; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; -import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/bizzclick.yaml", factory = YamlPropertySourceFactory.class) -public class BizzclickConfiguration { - - private static final String BIDDER_NAME = "bizzclick"; - - @Bean("bizzclickConfigurationProperties") - @ConfigurationProperties("adapters.bizzclick") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps bizzclickBidderDeps(BidderConfigurationProperties bizzclickConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(bizzclickConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new BizzclickBidder(config.getEndpoint(), mapper)) - .assemble(); - } -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java new file mode 100644 index 00000000000..1c57db91aba --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.blasto.BlastoBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/blasto.yaml", factory = YamlPropertySourceFactory.class) +public class BlastoConfiguration { + + private static final String BIDDER_NAME = "blasto"; + + @Bean("blastoConfigurationProperties") + @ConfigurationProperties("adapters.blasto") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps blastoBidderDeps(BidderConfigurationProperties blastoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(blastoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BlastoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BliinkBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BliinkBidderConfiguration.java index fcbbf49122e..6ce0d022f04 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BliinkBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BliinkBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bliink.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BlueSeaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlueSeaConfiguration.java index aa988d6c75f..823396b8b5d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BlueSeaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlueSeaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bluesea.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java index 91fd517a133..c9c7bcbebfa 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/bmtm.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BoldwinConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BoldwinConfiguration.java index bb648cf6b0b..f062311c4fa 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BoldwinConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BoldwinConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/boldwin.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BraveConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BraveConfiguration.java index 5dc013e8f7b..de364ab4bc0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BraveConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BraveConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/brave.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BwxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BwxConfiguration.java new file mode 100644 index 00000000000..6b8ce8bf7d2 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BwxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bwx.BwxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/bwx.yaml", factory = YamlPropertySourceFactory.class) +public class BwxConfiguration { + + private static final String BIDDER_NAME = "bwx"; + + @Bean("bwxConfigurationProperties") + @ConfigurationProperties("adapters.bwx") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bwxBidderDeps(BidderConfigurationProperties bwxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bwxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BwxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CcxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CcxConfiguration.java deleted file mode 100644 index de7e92ad89c..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/CcxConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.ccx.CcxBidder; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; -import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/ccx.yaml", factory = YamlPropertySourceFactory.class) -public class CcxConfiguration { - - private static final String BIDDER_NAME = "ccx"; - - @Bean("ccxConfigurationProperties") - @ConfigurationProperties("adapters.ccx") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps ccxBidderDeps( - BidderConfigurationProperties ccxConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(ccxConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new CcxBidder(config.getEndpoint(), mapper)) - .assemble(); - } - -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CointrafficConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CointrafficConfiguration.java new file mode 100644 index 00000000000..4eb4c660adb --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/CointrafficConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.cointraffic.CointrafficBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/cointraffic.yaml", factory = YamlPropertySourceFactory.class) +public class CointrafficConfiguration { + + private static final String BIDDER_NAME = "cointraffic"; + + @Bean("cointrafficConfigurationProperties") + @ConfigurationProperties("adapters.cointraffic") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps cointrafficBidderDeps(BidderConfigurationProperties cointrafficConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(cointrafficConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new CointrafficBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CoinzillaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CoinzillaConfiguration.java index cd402e4ec76..896bd31ff10 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/CoinzillaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/CoinzillaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/coinzilla.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java index 4187c7649b5..8d466e2e842 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/colossus.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CompassConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CompassConfiguration.java index 3f45046b918..e638d721fd0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/CompassConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/CompassConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/compass.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConcertConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConcertConfiguration.java new file mode 100644 index 00000000000..6ee644c4b59 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConcertConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.concert.ConcertBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/concert.yaml", factory = YamlPropertySourceFactory.class) +public class ConcertConfiguration { + + private static final String BIDDER_NAME = "concert"; + + @Bean("concertConfigurationProperties") + @ConfigurationProperties("adapters.concert") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps concertBidderDeps(BidderConfigurationProperties concertConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(concertConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ConcertBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java index ebf4225dedb..e076c7b35e6 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.connectad.ConnectadBidder; +import org.prebid.server.bidder.connectad.ConnectAdBidder; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/connectad.yaml", factory = YamlPropertySourceFactory.class) @@ -35,7 +35,7 @@ BidderDeps connectadBidderDeps(BidderConfigurationProperties connectadConfigurat return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(connectadConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new ConnectadBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new ConnectAdBidder(config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java index 5de2e87645c..70a31dfdddf 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/consumable.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java new file mode 100644 index 00000000000..61340987965 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.copper6ssp.Copper6SspBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/copper6ssp.yaml", factory = YamlPropertySourceFactory.class) +public class Copper6SspConfiguration { + + private static final String BIDDER_NAME = "copper6ssp"; + + @Bean("copper6sspConfigurationProperties") + @ConfigurationProperties("adapters.copper6ssp") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps copper6sspBidderDeps(BidderConfigurationProperties copper6sspConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(copper6sspConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new Copper6SspBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java index 33bd952d33f..b247970937b 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/cpmstar.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java index 698f5c3b83a..5693167b62e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/criteo.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java index 49b28ee132a..9d5d4fd9b08 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/datablocks.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java index 2e7da078a2c..c1eb0e2b1e3 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/decenterads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java index 146989a2ef7..ac14dd71c2e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/deepintent.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DefineMediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DefineMediaConfiguration.java new file mode 100644 index 00000000000..f0026d2e9e7 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/DefineMediaConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.definemedia.DefineMediaBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/definemedia.yaml", factory = YamlPropertySourceFactory.class) +public class DefineMediaConfiguration { + + private static final String BIDDER_NAME = "definemedia"; + + @Bean("definemediaConfigurationProperties") + @ConfigurationProperties("adapters.definemedia") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps definemediaBidderDeps(BidderConfigurationProperties definemediaConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(definemediaConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new DefineMediaBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DianomiBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DianomiBidderConfiguration.java index 7fe68311380..e9938a9d632 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/DianomiBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/DianomiBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/dianomi.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DisplayioConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DisplayioConfiguration.java new file mode 100644 index 00000000000..9f153fda821 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/DisplayioConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.displayio.DisplayioBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/displayio.yaml", factory = YamlPropertySourceFactory.class) +public class DisplayioConfiguration { + + private static final String BIDDER_NAME = "displayio"; + + @Bean("displayioConfigurationProperties") + @ConfigurationProperties("adapters.displayio") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps displayioBidderDeps(BidderConfigurationProperties displayioConfigurationProperties, + CurrencyConversionService currencyConversionService, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(displayioConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new DisplayioBidder(currencyConversionService, config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java index 506a4105a11..a80323c031c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/dmx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DriftpixelConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DriftpixelConfiguration.java new file mode 100644 index 00000000000..d44eeed42d4 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/DriftpixelConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.driftpixel.DriftpixelBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/driftpixel.yaml", factory = YamlPropertySourceFactory.class) +public class DriftpixelConfiguration { + + private static final String BIDDER_NAME = "driftpixel"; + + @Bean("driftpixelConfigurationProperties") + @ConfigurationProperties("adapters.driftpixel") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps driftpixelBidderDeps(BidderConfigurationProperties driftpixelConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(driftpixelConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new DriftpixelBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DxKultureBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DxKultureBidderConfiguration.java index e4ef4b92a64..658a2c8743c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/DxKultureBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/DxKultureBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/dxkulture.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Edge226Configuration.java b/src/main/java/org/prebid/server/spring/config/bidder/Edge226Configuration.java index 754a07a970c..03ecfc2add9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/Edge226Configuration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/Edge226Configuration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/edge226.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EmtvConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EmtvConfiguration.java index 6bd8f2125d1..3c5a934a8bc 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/EmtvConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/EmtvConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/emtv.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java index 0ecde28142a..64855592dbe 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/emxdigital.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java index 491813e0596..f67ab6d9ff5 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/eplanning.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java index f3c011f4146..88776cd74ad 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/epom.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EpsilonConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EpsilonConfiguration.java index 3025a45289c..f4daeba000a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/EpsilonConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/EpsilonConfiguration.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.epsilon.EpsilonBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -17,8 +18,8 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; @Configuration @PropertySource(value = "classpath:/bidder-config/epsilon.yaml", factory = YamlPropertySourceFactory.class) @@ -34,8 +35,9 @@ EpsilonConfigurationProperties configurationProperties() { @Bean BidderDeps epsilonBidderDeps(EpsilonConfigurationProperties epsilonConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(epsilonConfigurationProperties) @@ -44,7 +46,8 @@ BidderDeps epsilonBidderDeps(EpsilonConfigurationProperties epsilonConfiguration new EpsilonBidder( config.getEndpoint(), epsilonConfigurationProperties.getGenerateBidId(), - mapper)) + mapper, + currencyConversionService)) .assemble(); } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java new file mode 100644 index 00000000000..29bd855b91b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.escalax.EscalaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/escalax.yaml", factory = YamlPropertySourceFactory.class) +public class EscalaxConfiguration { + + private static final String BIDDER_NAME = "escalax"; + + @Bean("escalaxConfigurationProperties") + @ConfigurationProperties("adapters.escalax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps escalaxBidderDeps(BidderConfigurationProperties escalaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(escalaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new EscalaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java index ef32fda4196..9757923928e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/evolution.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FlippConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FlippConfiguration.java index 1f3ab8db2d3..e2e36373073 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/FlippConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/FlippConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/flipp.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FreewheelSSPConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FreewheelSSPConfiguration.java index d58278d9e64..0ac29fb2ad0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/FreewheelSSPConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/FreewheelSSPConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/freewheelssp.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FrvrAdnBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FrvrAdnBidderConfiguration.java index 5c772da6eca..f4c903a4968 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/FrvrAdnBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/FrvrAdnBidderConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/frvradn.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java index 5f4e871d55f..141c4ddd313 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/gamma.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java index 13bf512b415..15d37d3b5b8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/gamoshi.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GenericBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GenericBidderConfiguration.java index 0b5b46f2b81..23f1e2f2928 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GenericBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GenericBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/generic.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GlobalsunConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GlobalsunConfiguration.java index dfd0a7ffc18..2eca33cefcd 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GlobalsunConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GlobalsunConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/globalsun.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GothamAdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GothamAdsConfiguration.java index 029f9423886..97691f8f563 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GothamAdsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GothamAdsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/gothamads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java index a39811d4e5e..f7ddefef701 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/grid.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java index 72b2f86ec71..7970022d1e3 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/gumgum.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/HuaweiAdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/HuaweiAdsConfiguration.java index 9030d5934c7..ba69ed885b1 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/HuaweiAdsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/HuaweiAdsConfiguration.java @@ -27,9 +27,9 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.util.List; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ImdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ImdsConfiguration.java index 3128eab5de9..020dd0e6936 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ImdsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ImdsConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/imds.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ImpactifyConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ImpactifyConfiguration.java index 6daed5621b3..af1fe09228f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ImpactifyConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ImpactifyConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/impactify.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java index 54631f271e0..4003ed66438 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/improvedigital.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InfytvConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InfytvConfiguration.java deleted file mode 100644 index 979ba6f7fcb..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/InfytvConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.infytv.InfytvBidder; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; -import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/infytv.yaml", factory = YamlPropertySourceFactory.class) -public class InfytvConfiguration { - - private static final String BIDDER_NAME = "infytv"; - - @Bean("infytvConfigurationProperties") - @ConfigurationProperties("adapters.infytv") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps infytvBidderDeps(BidderConfigurationProperties infytvConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(infytvConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new InfytvBidder(config.getEndpoint(), mapper)) - .assemble(); - } - -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java index 981654fc9a2..1b2f21ec06e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/inmobi.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java index fb2cf581f15..4dbc0a2ab0b 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/interactiveoffers.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/IntertechConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/IntertechConfiguration.java index 066560a4e17..a6a5ea7c736 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/IntertechConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/IntertechConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/intertech.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java index 2d864f17c14..2111af79fb2 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/invibes.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/IqxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/IqxConfiguration.java index ff59d0298f0..189ad72adda 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/IqxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/IqxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/iqx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/IqzoneConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/IqzoneConfiguration.java index 87e7b5aabd1..4b9ddff193c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/IqzoneConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/IqzoneConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/iqzone.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java index 9d6218b3293..49a1bdbea69 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/ix.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java index 313fdeee64d..a3110e233ff 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/jixie.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KargoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KargoConfiguration.java index e434ebeb907..d01076cae25 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/KargoConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/KargoConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/kargo.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java index 555b75fa6e1..94e953d2506 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/kayzen.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java index 49e026b92c0..c7c5b689c0c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/kidoz.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KiviAdsBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KiviAdsBidderConfiguration.java index a2472886876..816ff86dac5 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/KiviAdsBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/KiviAdsBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/kiviads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java index 581f3c3da33..4b271d5cfac 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/krushmedia.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LemmaDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LemmaDigitalConfiguration.java index c36ad73abc4..3cdc83faed4 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LemmaDigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LemmaDigitalConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/lemmadigital.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java deleted file mode 100644 index 70bab296c4e..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.liftoff.LiftoffBidder; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; -import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/liftoff.yaml", factory = YamlPropertySourceFactory.class) -public class LiftoffConfiguration { - - private static final String BIDDER_NAME = "liftoff"; - - @Bean("liftoffConfigurationProperties") - @ConfigurationProperties("adapters.liftoff") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps liftoffBidderDeps(BidderConfigurationProperties liftoffConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - CurrencyConversionService currencyConversionService, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(liftoffConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new LiftoffBidder(config.getEndpoint(), currencyConversionService, mapper)) - .assemble(); - } -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LimeLightDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LimeLightDigitalConfiguration.java index cedf0ec590a..a94fc4020a2 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LimeLightDigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LimeLightDigitalConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/limelightDigital.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LmKiviAdsBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LmKiviAdsBidderConfiguration.java index 568b922a644..cca3ccf4dfe 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LmKiviAdsBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LmKiviAdsBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/lmkiviads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java index a4a9ccd5cae..b18f98fd753 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/lockerdome.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LoganConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LoganConfiguration.java index 7b218776294..424c64cd7a0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LoganConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LoganConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/logan.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java index 02a59440a50..5063fd1c040 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/logicad.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java index 9de12f3a137..757f8059ed7 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java @@ -31,7 +31,6 @@ BidderConfigurationProperties configurationProperties() { BidderDeps loopmeBidderDeps(BidderConfigurationProperties loopmeConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(loopmeConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LoyalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LoyalConfiguration.java new file mode 100644 index 00000000000..2f42de9a763 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/LoyalConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.loyal.LoyalBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/loyal.yaml", factory = YamlPropertySourceFactory.class) +public class LoyalConfiguration { + + private static final String BIDDER_NAME = "loyal"; + + @Bean("loyalConfigurationProperties") + @ConfigurationProperties("adapters.loyal") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps loaylBidderDeps(BidderConfigurationProperties loyalConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(loyalConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new LoyalBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java index cad75586f80..96076560f70 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/lunamedia.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java index 2d1ff24a6ef..938bc20e96c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/madvertise.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java index 53e9c19d7c9..99012d82a84 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/marsmedia.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MediaGoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MediaGoConfiguration.java new file mode 100644 index 00000000000..acef4e6f035 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MediaGoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.mediago.MediaGoBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/mediago.yaml", factory = YamlPropertySourceFactory.class) +public class MediaGoConfiguration { + + private static final String BIDDER_NAME = "mediago"; + + @Bean("mediagoConfigurationProperties") + @ConfigurationProperties("adapters.mediago") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps mediagoBidderDeps(BidderConfigurationProperties mediagoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(mediagoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MediaGoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java index 6f5a10c5e32..359f630febf 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/medianet.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java new file mode 100644 index 00000000000..797ec11730c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.melozen.MeloZenBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/melozen.yaml", factory = YamlPropertySourceFactory.class) +public class MeloZenConfiguration { + + private static final String BIDDER_NAME = "melozen"; + + @Bean("melozenConfigurationProperties") + @ConfigurationProperties("adapters.melozen") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps melozenBidderDeps(BidderConfigurationProperties melozenConfigurationProperties, + CurrencyConversionService currencyConversionService, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(melozenConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MeloZenBidder(currencyConversionService, config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java new file mode 100644 index 00000000000..df258ba30cd --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.metax.MetaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/metax.yaml", factory = YamlPropertySourceFactory.class) +public class MetaxConfiguration { + + private static final String BIDDER_NAME = "metax"; + + @Bean("metaxConfigurationProperties") + @ConfigurationProperties("adapters.metax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps metaxBidderDeps(BidderConfigurationProperties metaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(metaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MetaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java index 192ff7a9083..893e30c424f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/mgid.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MgidxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MgidxConfiguration.java index 521e0692120..f891d3f44d7 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MgidxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MgidxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/mgidx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java index 6584d277a2c..3b01a2f482a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MinuteMediaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/minutemedia.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java new file mode 100644 index 00000000000..1c9c7fd355f --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.missena.MissenaBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/missena.yaml", factory = YamlPropertySourceFactory.class) +public class MissenaConfiguration { + + private static final String BIDDER_NAME = "missena"; + + @Bean("missenaConfigurationProperties") + @ConfigurationProperties("adapters.missena") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps missenaBidderDeps(BidderConfigurationProperties missenaConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(missenaConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MissenaBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java index fdebfa6fa42..bb8c80c037d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/mobfoxpb.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java index 4c78c3310f8..6496f517007 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/mobilefuse.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MotorikConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MotorikConfiguration.java index 690c6468e45..e68ba630872 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MotorikConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MotorikConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/motorik.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java index 320f0a249ec..16fbbb37aca 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/NextMillenniumConfiguration.java @@ -16,7 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.util.List; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java index 840d5c647d3..52079d032ef 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/nobid.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OmsBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OmsBidderConfiguration.java index e9ac40cc76a..47cdeb08045 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OmsBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OmsBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/oms.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java index 8c5a86e9ec9..ce37fc92ce2 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/onetag.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OpenWebConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OpenWebConfiguration.java index 2c3ed5daac1..370839be501 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OpenWebConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OpenWebConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/openweb.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java index a936202776e..5e3886d5814 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/openx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OperaadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OperaadsConfiguration.java index 9eae2d808af..8b5055b4c29 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OperaadsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OperaadsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/operaads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java new file mode 100644 index 00000000000..8c5fbc6abe2 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.oraki.OrakiBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/oraki.yaml", factory = YamlPropertySourceFactory.class) +public class OrakiConfiguration { + + private static final String BIDDER_NAME = "oraki"; + + @Bean("orakiConfigurationProperties") + @ConfigurationProperties("adapters.oraki") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps orakiBidderDeps(BidderConfigurationProperties orakiConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(orakiConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OrakiBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java index e41814195fd..0e9bbcb29a6 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/orbidder.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java index 1e5353a989a..194dd8ca14a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/outbrain.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java new file mode 100644 index 00000000000..b34028b4221 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.ownadx.OwnAdxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/ownadx.yaml", factory = YamlPropertySourceFactory.class) +public class OwnAdxBidderConfiguration { + + private static final String BIDDER_NAME = "ownadx"; + + @Bean("ownAdxConfigurationProperties") + @ConfigurationProperties("adapters.ownadx") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps ownAdxBidderDeps(BidderConfigurationProperties ownAdxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(ownAdxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OwnAdxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java index e3ba1595664..2cd2c3b361c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/pangle.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java index 80304cbc659..1e449c950d6 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/pgamssp.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PlaydigoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PlaydigoConfiguration.java new file mode 100644 index 00000000000..75ae60eb5af --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/PlaydigoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.playdigo.PlaydigoBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/playdigo.yaml", factory = YamlPropertySourceFactory.class) +public class PlaydigoConfiguration { + + private static final String BIDDER_NAME = "playdigo"; + + @Bean("playdigoConfigurationProperties") + @ConfigurationProperties("adapters.playdigo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps playdigoBidderDeps(BidderConfigurationProperties playdigoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(playdigoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new PlaydigoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PrecisoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PrecisoConfiguration.java index a83140b7868..dfa65428aac 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PrecisoConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PrecisoConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/preciso.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java index 607b10b532a..2fa5477630f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/pubmatic.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java index 32409c06aaa..5e1ec608cc5 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/pubnative.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java new file mode 100644 index 00000000000..e8fd5755a58 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.pubrise.PubriseBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/pubrise.yaml", factory = YamlPropertySourceFactory.class) +public class PubriseConfiguration { + + private static final String BIDDER_NAME = "pubrise"; + + @Bean("pubriseConfigurationProperties") + @ConfigurationProperties("adapters.pubrise") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps pubriseBidderDeps(BidderConfigurationProperties pubriseConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(pubriseConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new PubriseBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java index bbf00524f65..91ca1b19220 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/pulsepoint.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PwbidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PwbidConfiguration.java index 24888ef7d7f..d076873bdf0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PwbidConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PwbidConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/pwbid.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java new file mode 100644 index 00000000000..0ccfe750403 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.qt.QtBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/qt.yaml", factory = YamlPropertySourceFactory.class) +public class QtConfiguration { + + private static final String BIDDER_NAME = "qt"; + + @Bean("qtConfigurationProperties") + @ConfigurationProperties("adapters.qt") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps qtBidderDeps(BidderConfigurationProperties qtConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(qtConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new QtBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ReadPeakConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ReadPeakConfiguration.java new file mode 100644 index 00000000000..351d6b121ac --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ReadPeakConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.readpeak.ReadPeakBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/readpeak.yaml", factory = YamlPropertySourceFactory.class) +public class ReadPeakConfiguration { + + private static final String BIDDER_NAME = "readpeak"; + + @Bean("readpeakConfigurationProperties") + @ConfigurationProperties("adapters.readpeak") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps readpeakBidderDeps(BidderConfigurationProperties readpeakConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(readpeakConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ReadPeakBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RelevantDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RelevantDigitalConfiguration.java index 15f39a644c1..b25ca14eb32 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RelevantDigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RelevantDigitalConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/relevantdigital.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java index 7111d8ad9ff..4e4de161f66 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/resetdigital.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java index 6435aa150c3..c21bf079b70 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/revcontent.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RichaudienceConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RichaudienceConfiguration.java index c343830ace4..fe8c2a00523 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RichaudienceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RichaudienceConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/richaudience.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RiseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RiseConfiguration.java index 87a2af115f0..c54c39cd6c8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RiseConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RiseConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/rise.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RoulaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RoulaxConfiguration.java new file mode 100644 index 00000000000..01ba4e70e3d --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/RoulaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.roulax.RoulaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/roulax.yaml", factory = YamlPropertySourceFactory.class) +public class RoulaxConfiguration { + + private static final String BIDDER_NAME = "roulax"; + + @Bean("roulaxConfigurationProperties") + @ConfigurationProperties("adapters.roulax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps roulaxBidderDeps(BidderConfigurationProperties roulaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(roulaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new RoulaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java index 3834fa0b878..2b99afafad9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/rtbhouse.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java index feb9f1c87c3..89f80e439aa 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java @@ -12,6 +12,7 @@ import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.version.PrebidVersionProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -19,9 +20,9 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; @Configuration @PropertySource(value = "classpath:/bidder-config/rubicon.yaml", factory = YamlPropertySourceFactory.class) @@ -40,6 +41,7 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration @NotBlank @Value("${external-url}") String externalUrl, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) @@ -47,13 +49,16 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration .usersyncerCreator(UsersyncerCreator.create(externalUrl)) .bidderCreator(config -> new RubiconBidder( + BIDDER_NAME, config.getEndpoint(), + externalUrl, config.getXapi().getUsername(), config.getXapi().getPassword(), config.getMetaInfo().getSupportedVendors(), config.getGenerateBidId(), currencyConversionService, floorResolver, + versionProvider, mapper)) .assemble(); } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java index f7d7248cbef..ab466600f26 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/salunamedia.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ScreencoreConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ScreencoreConfiguration.java index 74e1f39ff52..3cf155022e9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ScreencoreConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ScreencoreConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/screencore.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SeedingAllianceBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SeedingAllianceBidderConfiguration.java index 5ba2c7ae1ae..426950a684a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SeedingAllianceBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SeedingAllianceBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/seedingAlliance.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java index dd0a0abcbf1..3e75a34f8cc 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java @@ -15,7 +15,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/sharethrough.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SilverPushConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SilverPushConfiguration.java index b26e78fcd64..b3dcb5baf90 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SilverPushConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SilverPushConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/silverpush.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java index 98acc635472..e26433fb580 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/silvermob.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java index 28c50df1619..0b72987e6f9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/smilewanted.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java index 06ff89f6ee7..0851bd87d7d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.time.Clock; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java index 7cf0551db62..5264a6a3e77 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/smartadserver.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmarthubConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmarthubConfiguration.java index a19e364b949..218f7f8ae96 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmarthubConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmarthubConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/smarthub.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java index 33728c7499e..f0a5f22622c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/smartrtb.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartxConfiguration.java index ebf11d6a804..5b6c08b6dd3 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/smartx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java index 730d822633b..a5de7f27094 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/smartyads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmrtconnectConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmrtconnectConfiguration.java new file mode 100644 index 00000000000..24eb4d44996 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmrtconnectConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.smrtconnect.SmrtconnectBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/smrtconnect.yaml", factory = YamlPropertySourceFactory.class) +public class SmrtconnectConfiguration { + + private static final String BIDDER_NAME = "smrtconnect"; + + @Bean("smrtconnectConfigurationProperties") + @ConfigurationProperties("adapters.smrtconnect") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps smrtconnectBidderDeps(BidderConfigurationProperties smrtconnectConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(smrtconnectConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SmrtconnectBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java index 039a57f74b5..f640fe9f34d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.sonobi.SonobiBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -13,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/sonobi.yaml", factory = YamlPropertySourceFactory.class) @@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps sonobiBidderDeps(BidderConfigurationProperties sonobiConfigurationProperties, + CurrencyConversionService currencyConversionService, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(sonobiConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new SonobiBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new SonobiBidder(currencyConversionService, config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java index 281987c0bc6..4de292e9184 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/sovrn.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SovrnXspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SovrnXspConfiguration.java index 3dcd9a2a53e..a494790bb71 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SovrnXspConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SovrnXspConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/sovrnXsp.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SspbcBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SspbcBidderConfiguration.java index 8d2e8a01459..95ff7edcf08 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SspbcBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SspbcBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/sspbc.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java index 242a52f941c..f33c72e5027 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/stroeercore.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java index 99d30628a61..ef917b09bc5 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TaboolaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/taboola.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java index 846ad2f1057..352ec244ecd 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.time.Clock; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TeadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TeadsConfiguration.java index 699cd016b9a..a1cb6c26c99 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TeadsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TeadsConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/teads.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java index 2f12146901c..9be27f6bc33 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/telaria.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java new file mode 100644 index 00000000000..899c29d2293 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java @@ -0,0 +1,62 @@ +package org.prebid.server.spring.config.bidder; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.thetradedesk.TheTradeDeskBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/thetradedesk.yaml", factory = YamlPropertySourceFactory.class) +public class TheTradeDeskConfiguration { + + private static final String BIDDER_NAME = "thetradedesk"; + + @Bean("thetradedeskConfigurationProperties") + @ConfigurationProperties("adapters.thetradedesk") + TheTradeDeskConfigurationProperties configurationProperties() { + return new TheTradeDeskConfigurationProperties(); + } + + @Bean + BidderDeps theTradeDeskBidderDeps(TheTradeDeskConfigurationProperties theTradeDeskConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(theTradeDeskConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TheTradeDeskBidder( + config.getEndpoint(), + mapper, + config.getExtraInfo().getSupplyId()) + ).assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class TheTradeDeskConfigurationProperties extends BidderConfigurationProperties { + + private ExtraInfo extraInfo = new ExtraInfo(); + } + + @Data + @NoArgsConstructor + private static class ExtraInfo { + + String supplyId; + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TheadxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TheadxConfiguration.java new file mode 100644 index 00000000000..6bb9e397e05 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TheadxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.theadx.TheadxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/theadx.yaml", factory = YamlPropertySourceFactory.class) +public class TheadxConfiguration { + + private static final String BIDDER_NAME = "theadx"; + + @Bean("theadxConfigurationProperties") + @ConfigurationProperties("adapters.theadx") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps theadxBidderDeps(BidderConfigurationProperties theadxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(theadxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TheadxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ThirtyThreeAcrossConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ThirtyThreeAcrossConfiguration.java index c114867c505..1a04588e973 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ThirtyThreeAcrossConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ThirtyThreeAcrossConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/thirtythreeacross.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TpmnAdnBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TpmnAdnBidderConfiguration.java index 224f0fab7bd..0a40807f424 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TpmnAdnBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TpmnAdnBidderConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/tpmn.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java new file mode 100644 index 00000000000..8bd04ffd8f3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.tradplus.TradPlusBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/tradplus.yaml", factory = YamlPropertySourceFactory.class) +public class TradPlusBidderConfiguration { + + private static final String BIDDER_NAME = "tradplus"; + + @Bean("tradplusConfigurationProperties") + @ConfigurationProperties("adapters.tradplus") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps tradplusBidderDeps(BidderConfigurationProperties tradplusConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(tradplusConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TradPlusBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TrafficGateConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TrafficGateConfiguration.java index 56a25a94d7e..33e0ecaa1e8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TrafficGateConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TrafficGateConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/trafficgate.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java index b730143bc95..301b26478c1 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/triplelift.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java index c1a7ee5b1ca..11ef984aa78 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java @@ -18,8 +18,8 @@ import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.List; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TrustedstackConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TrustedstackConfiguration.java new file mode 100644 index 00000000000..679792ee341 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TrustedstackConfiguration.java @@ -0,0 +1,42 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.trustedstack.TrustedstackBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/trustedstack.yaml", factory = YamlPropertySourceFactory.class) +public class TrustedstackConfiguration { + + private static final String BIDDER_NAME = "trustedstack"; + + @Bean("trustedstackConfigurationProperties") + @ConfigurationProperties("adapters.trustedstack") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps trustedstackBidderDeps(BidderConfigurationProperties trustedstackConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(trustedstackConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> + new TrustedstackBidder(config.getEndpoint(), externalUrl, mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java index 3fc172aeab4..6e2e4067b7d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/ucfunnel.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UndertoneConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UndertoneConfiguration.java index 60a1dc415c5..a51380096ea 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/UndertoneConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/UndertoneConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/undertone.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java index 21d9e820f3d..7905fc384f0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/unicorn.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java index e57d436165c..44ed956b391 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/unruly.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VidazooConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VidazooConfiguration.java new file mode 100644 index 00000000000..bf83a8bcaac --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/VidazooConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.vidazoo.VidazooBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/vidazoo.yaml", factory = YamlPropertySourceFactory.class) +public class VidazooConfiguration { + + private static final String BIDDER_NAME = "vidazoo"; + + @Bean("vidazooConfigurationProperties") + @ConfigurationProperties("adapters.vidazoo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps vidazooBidderDeps(BidderConfigurationProperties vidazooConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(vidazooConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new VidazooBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VideoHeroesConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VideoHeroesConfiguration.java index e7ef3ddfc65..f501af44cf4 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VideoHeroesConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VideoHeroesConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/videoheroes.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VideobyteConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VideobyteConfiguration.java index 59bf7326947..a25f213c6a1 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VideobyteConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VideobyteConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/videobyte.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VidoomyConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VidoomyConfiguration.java index aeaf95f1ccc..45806a99bf0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VidoomyConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VidoomyConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/vidoomy.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VisibleMeasuresConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VisibleMeasuresConfiguration.java index 21a4ecd20a5..9dfc739b5ff 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VisibleMeasuresConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VisibleMeasuresConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/visiblemeasures.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java index 57fbb9c8987..a3585633163 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/visx.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VoxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VoxConfiguration.java index 2524a5b2fce..0d0974c87d0 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VoxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VoxConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/vox.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java index 1343e007a68..73e0b9bacc5 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/vrtcal.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java new file mode 100644 index 00000000000..a2bc6bd427e --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.vungle.VungleBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/vungle.yaml", factory = YamlPropertySourceFactory.class) +public class VungleConfiguration { + + private static final String BIDDER_NAME = "vungle"; + + @Bean("vungleConfigurationProperties") + @ConfigurationProperties("adapters.vungle") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps vungleBidderDeps(BidderConfigurationProperties vungleConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(vungleConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new VungleBidder(config.getEndpoint(), currencyConversionService, mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/XeworksBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/XeworksBidderConfiguration.java index ddedb314725..be4681a98e7 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/XeworksBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/XeworksBidderConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/xeworks.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java index 14b0397a148..9cd3ed1249b 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YahooAdsConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/yahooAds.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YandexConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YandexConfiguration.java index 629f8f63b11..5f1afa3398f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YandexConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YandexConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/yandex.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YeahmobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YeahmobiConfiguration.java index 8dc6c20d857..1bc55fd4ef8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YeahmobiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YeahmobiConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/yeahmobi.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YearxeroConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YearxeroConfiguration.java index 2a278e1d88a..1ce349de7dd 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YearxeroConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YearxeroConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/yearxero.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java index 0d2dc0a31ad..5bf1f496477 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import java.time.Clock; @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java index 3562e02acfe..c8d9c6eff62 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.yieldmo.YieldmoBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -13,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/yieldmo.yaml", factory = YamlPropertySourceFactory.class) @@ -30,12 +31,13 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps yieldmoBidderDeps(BidderConfigurationProperties yieldmoConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(yieldmoConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new YieldmoBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new YieldmoBidder(config.getEndpoint(), currencyConversionService, mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java index aa1c028b653..8e32a38e2d3 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/yieldone.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZMaticooBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZMaticooBidderConfiguration.java new file mode 100644 index 00000000000..540a58a67ec --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ZMaticooBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.zmaticoo.ZMaticooBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/zmaticoo.yaml", factory = YamlPropertySourceFactory.class) +public class ZMaticooBidderConfiguration { + + private static final String BIDDER_NAME = "zmaticoo"; + + @Bean("zmaticooConfigurationProperties") + @ConfigurationProperties("adapters.zmaticoo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps zmaticooBidderDeps(BidderConfigurationProperties zmaticooConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(zmaticooConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ZMaticooBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java index 87fb61aa20c..3430977f4db 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Configuration @PropertySource(value = "classpath:/bidder-config/zeroclickfraud.yaml", factory = YamlPropertySourceFactory.class) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java deleted file mode 100644 index b2bd0662251..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.zeta_global_ssp.ZetaGlobalSspBidder; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; -import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/zeta_global_ssp.yaml", factory = YamlPropertySourceFactory.class) -public class ZetaGlobalSspConfiguration { - - private static final String BIDDER_NAME = "zeta_global_ssp"; - - @Bean - @ConfigurationProperties("adapters.zeta-global-ssp") - BidderConfigurationProperties zetaGlobalSspConfigurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps zetaGlobalSspBidderDeps(BidderConfigurationProperties zetaGlobalSspConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(zetaGlobalSspConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new ZetaGlobalSspBidder(config.getEndpoint(), mapper)) - .assemble(); - } - -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java index 63499df3804..0f5fde1d480 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java @@ -10,9 +10,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; -import javax.annotation.PostConstruct; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.annotation.PostConstruct; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Map; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/Debug.java b/src/main/java/org/prebid/server/spring/config/bidder/model/Debug.java index 2a88fc91e66..c1e44593d0d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/Debug.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/Debug.java @@ -4,7 +4,7 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; @Validated @Data diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/DefaultBidderConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/DefaultBidderConfigurationProperties.java index 99b0ca38829..7996a2e220c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/DefaultBidderConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/DefaultBidderConfigurationProperties.java @@ -4,7 +4,7 @@ import org.prebid.server.auction.versionconverter.OrtbVersion; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.List; import java.util.Map; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java index 0e46906f528..bffc274c35d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java @@ -4,8 +4,8 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.List; @Validated @@ -24,6 +24,8 @@ public class MetaInfo { private List supportedVendors; + private List currencyAccepted; + @NotNull private Integer vendorId; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/Ortb.java b/src/main/java/org/prebid/server/spring/config/bidder/model/Ortb.java index e3153ff8654..9d22bbd1de1 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/Ortb.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/Ortb.java @@ -6,7 +6,7 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; @Data @Validated diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java index 6acd9c68ce0..e67debfad6c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java @@ -4,7 +4,7 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; @Data @Validated diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncMethodConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncMethodConfigurationProperties.java index bba6fbc6826..fc9b685b339 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncMethodConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncMethodConfigurationProperties.java @@ -5,8 +5,8 @@ import org.prebid.server.bidder.UsersyncFormat; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; @Data @Validated diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java index 342ce998592..cd7553bb34a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java @@ -28,6 +28,7 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope metaInfo.getDoohMediaTypes(), metaInfo.getSupportedVendors(), metaInfo.getVendorId(), + metaInfo.getCurrencyAccepted(), configurationProperties.getPbsEnforcesCcpa(), configurationProperties.getModifyingVastXmlAllowed(), configurationProperties.getEndpointCompression(), diff --git a/src/main/java/org/prebid/server/spring/config/database/DatabaseConfiguration.java b/src/main/java/org/prebid/server/spring/config/database/DatabaseConfiguration.java index 69570b59622..6a1a1531f1c 100644 --- a/src/main/java/org/prebid/server/spring/config/database/DatabaseConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/database/DatabaseConfiguration.java @@ -1,17 +1,23 @@ package org.prebid.server.spring.config.database; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.jdbc.JDBCClient; +import io.vertx.mysqlclient.MySQLBuilder; +import io.vertx.mysqlclient.MySQLConnectOptions; +import io.vertx.pgclient.PgBuilder; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.helper.ParametrizedQueryHelper; +import org.prebid.server.settings.helper.ParametrizedQueryMySqlHelper; +import org.prebid.server.settings.helper.ParametrizedQueryPostgresHelper; import org.prebid.server.spring.config.database.model.ConnectionPoolSettings; import org.prebid.server.spring.config.database.model.DatabaseAddress; import org.prebid.server.spring.config.database.properties.DatabaseConfigurationProperties; import org.prebid.server.spring.config.model.CircuitBreakerProperties; import org.prebid.server.vertx.ContextRunner; -import org.prebid.server.vertx.jdbc.BasicJdbcClient; -import org.prebid.server.vertx.jdbc.CircuitBreakerSecuredJdbcClient; -import org.prebid.server.vertx.jdbc.JdbcClient; +import org.prebid.server.vertx.database.BasicDatabaseClient; +import org.prebid.server.vertx.database.CircuitBreakerSecuredDatabaseClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -21,48 +27,12 @@ import org.springframework.validation.annotation.Validated; import java.time.Clock; +import java.util.concurrent.TimeUnit; @Configuration @ConditionalOnExpression("'${settings.database.type}' == 'postgres' or '${settings.database.type}' == 'mysql'") public class DatabaseConfiguration { - @Bean - @ConditionalOnProperty(name = "settings.database.type", havingValue = "postgres") - DatabaseUrlFactory postgresUrlFactory() { - return "jdbc:postgresql://%s:%d/%s?ssl=false&socketTimeout=1&tcpKeepAlive=true"::formatted; - } - - @Bean - @ConditionalOnProperty(name = "settings.database.type", havingValue = "mysql") - DatabaseUrlFactory mySqlUrlFactory() { - return "jdbc:mysql://%s:%d/%s?useSSL=false&socketTimeout=1000&tcpKeepAlive=true"::formatted; - } - - @Bean - @ConditionalOnProperty(name = "settings.database.provider-class", havingValue = "hikari") - ConnectionPoolConfigurationFactory hikariConfigurationFactory() { - return (url, connectionPoolSettings) -> new JsonObject() - .put("jdbcUrl", url + "&allowPublicKeyRetrieval=true") - .put("username", connectionPoolSettings.getUser()) - .put("password", connectionPoolSettings.getPassword()) - .put("minimumIdle", connectionPoolSettings.getPoolSize()) - .put("maximumPoolSize", connectionPoolSettings.getPoolSize()) - .put("provider_class", "io.vertx.ext.jdbc.spi.impl.HikariCPDataSourceProvider"); - } - - @Bean - @ConditionalOnProperty(name = "settings.database.provider-class", havingValue = "c3p0") - ConnectionPoolConfigurationFactory c3p0ConfigurationFactory() { - return (url, connectionPoolSettings) -> new JsonObject() - .put("url", url) - .put("user", connectionPoolSettings.getUser()) - .put("password", connectionPoolSettings.getPassword()) - .put("initial_pool_size", connectionPoolSettings.getPoolSize()) - .put("min_pool_size", connectionPoolSettings.getPoolSize()) - .put("max_pool_size", connectionPoolSettings.getPoolSize()) - .put("provider_class", "io.vertx.ext.jdbc.spi.impl.C3P0DataSourceProvider"); - } - @Bean DatabaseAddress databaseAddress(DatabaseConfigurationProperties databaseConfigurationProperties) { return DatabaseAddress.of( @@ -75,37 +45,91 @@ DatabaseAddress databaseAddress(DatabaseConfigurationProperties databaseConfigur ConnectionPoolSettings connectionPoolSettings(DatabaseConfigurationProperties databaseConfigurationProperties) { return ConnectionPoolSettings.of( databaseConfigurationProperties.getPoolSize(), + databaseConfigurationProperties.getIdleConnectionTimeout(), + databaseConfigurationProperties.getEnablePreparedStatementCaching(), + databaseConfigurationProperties.getMaxPreparedStatementCacheSize(), databaseConfigurationProperties.getUser(), databaseConfigurationProperties.getPassword(), databaseConfigurationProperties.getType()); } @Bean - JDBCClient vertxJdbcClient(Vertx vertx, - DatabaseAddress databaseAddress, - ConnectionPoolSettings connectionPoolSettings, - DatabaseUrlFactory urlFactory, - ConnectionPoolConfigurationFactory configurationFactory) { - - final String databaseUrl = urlFactory.createUrl( - databaseAddress.getHost(), databaseAddress.getPort(), databaseAddress.getDatabaseName()); - - final JsonObject connectionPoolConfigurationProperties = configurationFactory.create( - databaseUrl, connectionPoolSettings); - final JsonObject databaseConfigurationProperties = new JsonObject() - .put("driver_class", connectionPoolSettings.getDatabaseType().jdbcDriver); - databaseConfigurationProperties.mergeIn(connectionPoolConfigurationProperties); - - return JDBCClient.createShared(vertx, databaseConfigurationProperties); + @ConfigurationProperties(prefix = "settings.database") + @Validated + public DatabaseConfigurationProperties databaseConfigurationProperties() { + return new DatabaseConfigurationProperties(); } @Bean - @ConditionalOnProperty(prefix = "settings.database.circuit-breaker", name = "enabled", havingValue = "false", - matchIfMissing = true) - BasicJdbcClient basicJdbcClient( - Vertx vertx, JDBCClient vertxJdbcClient, Metrics metrics, Clock clock, ContextRunner contextRunner) { + @ConditionalOnProperty(name = "settings.database.type", havingValue = "mysql") + ParametrizedQueryHelper mysqlParametrizedQueryHelper() { + return new ParametrizedQueryMySqlHelper(); + } - return createBasicJdbcClient(vertx, vertxJdbcClient, metrics, clock, contextRunner); + @Bean + @ConditionalOnProperty(name = "settings.database.type", havingValue = "postgres") + ParametrizedQueryHelper postgresParametrizedQueryHelper() { + return new ParametrizedQueryPostgresHelper(); + } + + @Bean + @ConditionalOnProperty(name = "settings.database.type", havingValue = "mysql") + Pool mysqlConnectionPool(Vertx vertx, + DatabaseAddress databaseAddress, + ConnectionPoolSettings connectionPoolSettings) { + + final MySQLConnectOptions sqlConnectOptions = new MySQLConnectOptions() + .setHost(databaseAddress.getHost()) + .setPort(databaseAddress.getPort()) + .setDatabase(databaseAddress.getDatabaseName()) + .setUser(connectionPoolSettings.getUser()) + .setPassword(connectionPoolSettings.getPassword()) + .setSsl(false) + .setTcpKeepAlive(true) + .setCachePreparedStatements(connectionPoolSettings.getEnablePreparedStatementCaching()) + .setPreparedStatementCacheMaxSize(connectionPoolSettings.getMaxPreparedStatementCacheSize()) + .setIdleTimeout(connectionPoolSettings.getIdleTimeout()) + .setIdleTimeoutUnit(TimeUnit.SECONDS); + + final PoolOptions poolOptions = new PoolOptions() + .setMaxSize(connectionPoolSettings.getPoolSize()); + + return MySQLBuilder + .pool() + .with(poolOptions) + .connectingTo(sqlConnectOptions) + .using(vertx) + .build(); + } + + @Bean + @ConditionalOnProperty(name = "settings.database.type", havingValue = "postgres") + Pool postgresConnectionPool(Vertx vertx, + DatabaseAddress databaseAddress, + ConnectionPoolSettings connectionPoolSettings) { + + final PgConnectOptions sqlConnectOptions = new PgConnectOptions() + .setHost(databaseAddress.getHost()) + .setPort(databaseAddress.getPort()) + .setDatabase(databaseAddress.getDatabaseName()) + .setUser(connectionPoolSettings.getUser()) + .setPassword(connectionPoolSettings.getPassword()) + .setSsl(false) + .setTcpKeepAlive(true) + .setCachePreparedStatements(connectionPoolSettings.getEnablePreparedStatementCaching()) + .setPreparedStatementCacheMaxSize(connectionPoolSettings.getMaxPreparedStatementCacheSize()) + .setIdleTimeout(connectionPoolSettings.getIdleTimeout()) + .setIdleTimeoutUnit(TimeUnit.SECONDS); + + final PoolOptions poolOptions = new PoolOptions() + .setMaxSize(connectionPoolSettings.getPoolSize()); + + return PgBuilder + .pool() + .with(poolOptions) + .connectingTo(sqlConnectOptions) + .using(vertx) + .build(); } @Bean @@ -115,31 +139,44 @@ CircuitBreakerProperties databaseCircuitBreakerProperties() { return new CircuitBreakerProperties(); } + @Bean + @ConditionalOnProperty(prefix = "settings.database.circuit-breaker", name = "enabled", havingValue = "false", + matchIfMissing = true) + BasicDatabaseClient basicDatabaseClient(Pool pool, Metrics metrics, Clock clock, ContextRunner contextRunner) { + + return createBasicDatabaseClient(pool, metrics, clock, contextRunner); + } + @Bean @ConditionalOnProperty(prefix = "settings.database.circuit-breaker", name = "enabled", havingValue = "true") - CircuitBreakerSecuredJdbcClient circuitBreakerSecuredJdbcClient( - Vertx vertx, JDBCClient vertxJdbcClient, Metrics metrics, Clock clock, ContextRunner contextRunner, + CircuitBreakerSecuredDatabaseClient circuitBreakerSecuredAsyncDatabaseClient( + Vertx vertx, + Pool pool, + Metrics metrics, + Clock clock, + ContextRunner contextRunner, @Qualifier("databaseCircuitBreakerProperties") CircuitBreakerProperties circuitBreakerProperties) { - final JdbcClient jdbcClient = createBasicJdbcClient(vertx, vertxJdbcClient, metrics, clock, contextRunner); - return new CircuitBreakerSecuredJdbcClient(vertx, jdbcClient, metrics, - circuitBreakerProperties.getOpeningThreshold(), circuitBreakerProperties.getOpeningIntervalMs(), - circuitBreakerProperties.getClosingIntervalMs(), clock); + final BasicDatabaseClient databaseClient = createBasicDatabaseClient(pool, metrics, clock, contextRunner); + return new CircuitBreakerSecuredDatabaseClient( + vertx, + databaseClient, + metrics, + circuitBreakerProperties.getOpeningThreshold(), + circuitBreakerProperties.getOpeningIntervalMs(), + circuitBreakerProperties.getClosingIntervalMs(), + clock); } - private static BasicJdbcClient createBasicJdbcClient( - Vertx vertx, JDBCClient vertxJdbcClient, Metrics metrics, Clock clock, ContextRunner contextRunner) { - final BasicJdbcClient basicJdbcClient = new BasicJdbcClient(vertx, vertxJdbcClient, metrics, clock); + private static BasicDatabaseClient createBasicDatabaseClient(Pool pool, + Metrics metrics, + Clock clock, + ContextRunner contextRunner) { - contextRunner.runOnServiceContext(promise -> basicJdbcClient.initialize().onComplete(promise)); + final BasicDatabaseClient basicDatabaseClient = new BasicDatabaseClient(pool, metrics, clock); - return basicJdbcClient; - } + contextRunner.runBlocking(promise -> basicDatabaseClient.initialize().onComplete(promise)); - @Bean - @ConfigurationProperties(prefix = "settings.database") - @Validated - public DatabaseConfigurationProperties databaseConfigurationProperties() { - return new DatabaseConfigurationProperties(); + return basicDatabaseClient; } } diff --git a/src/main/java/org/prebid/server/spring/config/database/model/ConnectionPoolSettings.java b/src/main/java/org/prebid/server/spring/config/database/model/ConnectionPoolSettings.java index a0ffb6fa5b0..92f929564d6 100644 --- a/src/main/java/org/prebid/server/spring/config/database/model/ConnectionPoolSettings.java +++ b/src/main/java/org/prebid/server/spring/config/database/model/ConnectionPoolSettings.java @@ -7,6 +7,12 @@ public class ConnectionPoolSettings { Integer poolSize; + Integer idleTimeout; + + Boolean enablePreparedStatementCaching; + + Integer maxPreparedStatementCacheSize; + String user; String password; diff --git a/src/main/java/org/prebid/server/spring/config/database/model/DatabasePoolType.java b/src/main/java/org/prebid/server/spring/config/database/model/DatabasePoolType.java deleted file mode 100644 index d3337549654..00000000000 --- a/src/main/java/org/prebid/server/spring/config/database/model/DatabasePoolType.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.spring.config.database.model; - -public enum DatabasePoolType { - - hikari, c3p0 -} diff --git a/src/main/java/org/prebid/server/spring/config/database/model/DatabaseType.java b/src/main/java/org/prebid/server/spring/config/database/model/DatabaseType.java index 8c94c1c0ced..fd7373a1aab 100644 --- a/src/main/java/org/prebid/server/spring/config/database/model/DatabaseType.java +++ b/src/main/java/org/prebid/server/spring/config/database/model/DatabaseType.java @@ -5,8 +5,6 @@ @AllArgsConstructor public enum DatabaseType { - postgres("org.postgresql.Driver"), - mysql("com.mysql.cj.jdbc.Driver"); - - public final String jdbcDriver; + postgres, + mysql } diff --git a/src/main/java/org/prebid/server/spring/config/database/properties/DatabaseConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/database/properties/DatabaseConfigurationProperties.java index e5b6198fb66..cd570100df3 100644 --- a/src/main/java/org/prebid/server/spring/config/database/properties/DatabaseConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/database/properties/DatabaseConfigurationProperties.java @@ -2,12 +2,12 @@ import lombok.Data; import lombok.NoArgsConstructor; -import org.prebid.server.spring.config.database.model.DatabasePoolType; import org.prebid.server.spring.config.database.model.DatabaseType; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; @Data @NoArgsConstructor @@ -18,6 +18,14 @@ public class DatabaseConfigurationProperties { @NotNull @Min(1) private Integer poolSize; + @NotNull + @PositiveOrZero + private Integer idleConnectionTimeout; + @NotNull + private Boolean enablePreparedStatementCaching; + @NotNull + @Min(1) + private Integer maxPreparedStatementCacheSize; @NotBlank private String host; @NotNull @@ -28,7 +36,4 @@ public class DatabaseConfigurationProperties { private String user; @NotBlank private String password; - @NotNull - private DatabasePoolType providerClass; } - diff --git a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java index 999b80ef47a..21d145bf826 100644 --- a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java @@ -1,5 +1,7 @@ package org.prebid.server.spring.config.metrics; +import org.slf4j.LoggerFactory; +import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.ConsoleReporter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.ScheduledReporter; @@ -11,7 +13,6 @@ import com.izettle.metrics.influxdb.InfluxDbHttpSender; import com.izettle.metrics.influxdb.InfluxDbReporter; import com.izettle.metrics.influxdb.InfluxDbSender; -import io.vertx.core.Vertx; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.ObjectUtils; @@ -20,8 +21,6 @@ import org.prebid.server.metric.Metrics; import org.prebid.server.metric.model.AccountMetricsVerbosityLevel; import org.prebid.server.spring.env.YamlPropertySourceFactory; -import org.prebid.server.vertx.CloseableAdapter; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -31,10 +30,9 @@ import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; -import javax.annotation.PostConstruct; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -47,12 +45,6 @@ public class MetricsConfiguration { public static final String METRIC_REGISTRY_NAME = "metric-registry"; - @Autowired(required = false) - private List reporters = Collections.emptyList(); - - @Autowired - private Vertx vertx; - @Bean @ConditionalOnProperty(prefix = "metrics.graphite", name = "enabled", havingValue = "true") ScheduledReporter graphiteReporter(GraphiteProperties graphiteProperties, MetricRegistry metricRegistry) { @@ -102,8 +94,22 @@ ScheduledReporter consoleReporter(ConsoleProperties consoleProperties, MetricReg } @Bean - Metrics metrics(@Value("${metrics.metricType}") CounterType counterType, MetricRegistry metricRegistry, + @ConditionalOnProperty(prefix = "metrics.logback", name = "enabled", havingValue = "true") + ScheduledReporter logReporter(MetricsLogProperties metricsLogProperties, MetricRegistry metricRegistry) { + final ScheduledReporter reporter = Slf4jReporter.forRegistry(metricRegistry) + .outputTo(LoggerFactory.getLogger(metricsLogProperties.getName())) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS).build(); + reporter.start(metricsLogProperties.getInterval(), TimeUnit.SECONDS); + + return reporter; + } + + @Bean + Metrics metrics(@Value("${metrics.metricType}") CounterType counterType, + MetricRegistry metricRegistry, AccountMetricsVerbosityResolver accountMetricsVerbosityResolver) { + return new Metrics(metricRegistry, counterType, accountMetricsVerbosityResolver); } @@ -128,13 +134,6 @@ AccountMetricsVerbosityResolver accountMetricsVerbosity(AccountsProperties accou accountsProperties.getDetailedVerbosity()); } - @PostConstruct - void registerReporterCloseHooks() { - reporters.stream() - .map(CloseableAdapter::new) - .forEach(closeable -> vertx.getOrCreateContext().addCloseHook(closeable)); - } - @Component @ConfigurationProperties(prefix = "metrics.graphite") @ConditionalOnProperty(prefix = "metrics.graphite", name = "enabled", havingValue = "true") @@ -199,6 +198,21 @@ private static class ConsoleProperties { private Integer interval; } + @Component + @ConfigurationProperties(prefix = "metrics.logback") + @ConditionalOnProperty(prefix = "metrics.logback", name = "enabled", havingValue = "true") + @Validated + @Data + @NoArgsConstructor + private static class MetricsLogProperties { + + @NotNull + @Min(1) + private Integer interval; + @NotBlank + private String name; + } + @Component @ConfigurationProperties(prefix = "metrics.accounts") @Validated diff --git a/src/main/java/org/prebid/server/spring/config/metrics/PrometheusConfiguration.java b/src/main/java/org/prebid/server/spring/config/metrics/PrometheusConfiguration.java index 77e0b942817..174db85731f 100644 --- a/src/main/java/org/prebid/server/spring/config/metrics/PrometheusConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/metrics/PrometheusConfiguration.java @@ -7,18 +7,17 @@ import io.prometheus.client.dropwizard.samplebuilder.SampleBuilder; import io.prometheus.client.vertx.MetricsHandler; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import io.vertx.core.net.SocketAddress; import io.vertx.ext.web.Router; import lombok.Data; import lombok.NoArgsConstructor; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.CounterType; import org.prebid.server.metric.Metrics; import org.prebid.server.metric.prometheus.NamespaceSubsystemSampleBuilder; -import org.prebid.server.vertx.ContextRunner; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.prebid.server.vertx.verticles.VerticleDefinition; +import org.prebid.server.vertx.verticles.server.ServerVerticle; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -26,15 +25,32 @@ import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; -import javax.annotation.PostConstruct; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.util.List; @Configuration +@ConditionalOnProperty(prefix = "metrics.prometheus", name = "enabled", havingValue = "true") public class PrometheusConfiguration { + private static final Logger logger = LoggerFactory.getLogger(PrometheusConfiguration.class); + + // TODO: Decide how to integrate this with ability to serve requests on unix domain socket + @Bean + public VerticleDefinition prometheusHttpServerVerticleDefinition( + PrometheusConfigurationProperties prometheusConfigurationProperties, + Router prometheusRouter, + DropwizardExports dropwizardExports) { + + CollectorRegistry.defaultRegistry.register(dropwizardExports); + + return VerticleDefinition.ofSingleInstance( + () -> new ServerVerticle( + "Prometheus Http Server", + SocketAddress.inetSocketAddress(prometheusConfigurationProperties.getPort(), "0.0.0.0"), + prometheusRouter)); + } + @Bean - @ConditionalOnBean(PrometheusConfigurationProperties.class) public SampleBuilder sampleBuilder(PrometheusConfigurationProperties prometheusConfigurationProperties, List mapperConfigs) { @@ -44,51 +60,20 @@ public SampleBuilder sampleBuilder(PrometheusConfigurationProperties prometheusC mapperConfigs); } - @Configuration - @ConditionalOnBean(PrometheusConfigurationProperties.class) - public static class PrometheusServerConfiguration { - private static final Logger logger = LoggerFactory.getLogger(PrometheusServerConfiguration.class); - - @Autowired - private ContextRunner contextRunner; - - @Autowired - private Vertx vertx; - - @Autowired - private MetricRegistry metricRegistry; - - @Autowired - private Metrics metrics; - - @Autowired - private PrometheusConfigurationProperties prometheusConfigurationProperties; - - @Autowired - private SampleBuilder sampleBuilder; - - @PostConstruct - public void startPrometheusServer() { - logger.info( - "Starting Prometheus Server on port {0,number,#}", - prometheusConfigurationProperties.getPort()); - - if (metrics.getCounterType() == CounterType.flushingCounter) { - logger.warn("Prometheus metric system: Metric type is flushingCounter."); - } - - final Router router = Router.router(vertx); - router.route("/metrics").handler(new MetricsHandler()); - - CollectorRegistry.defaultRegistry.register(new DropwizardExports(metricRegistry, sampleBuilder)); + @Bean + DropwizardExports dropwizardExports(Metrics metrics, MetricRegistry metricRegistry, SampleBuilder sampleBuilder) { + if (metrics.getCounterType() == CounterType.flushingCounter) { + logger.warn("Prometheus metric system: Metric type is flushingCounter."); + } - contextRunner.runOnServiceContext(promise -> - vertx.createHttpServer() - .requestHandler(router) - .listen(prometheusConfigurationProperties.getPort(), promise)); + return new DropwizardExports(metricRegistry, sampleBuilder); + } - logger.info("Successfully started Prometheus Server"); - } + @Bean + Router prometheusRouter(Vertx vertx) { + final Router router = Router.router(vertx); + router.route("/metrics").handler(new MetricsHandler()); + return router; } @Data diff --git a/src/main/java/org/prebid/server/spring/config/metrics/PrometheusMapperConfiguration.java b/src/main/java/org/prebid/server/spring/config/metrics/PrometheusMapperConfiguration.java index e9ea4ac3b29..ea7bb3989f7 100644 --- a/src/main/java/org/prebid/server/spring/config/metrics/PrometheusMapperConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/metrics/PrometheusMapperConfiguration.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Map; diff --git a/src/main/java/org/prebid/server/spring/config/model/CircuitBreakerProperties.java b/src/main/java/org/prebid/server/spring/config/model/CircuitBreakerProperties.java index cbc3e22f81e..969b5f35784 100644 --- a/src/main/java/org/prebid/server/spring/config/model/CircuitBreakerProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/CircuitBreakerProperties.java @@ -4,8 +4,8 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; @Validated @Data diff --git a/src/main/java/org/prebid/server/spring/config/model/ExponentialBackoffProperties.java b/src/main/java/org/prebid/server/spring/config/model/ExponentialBackoffProperties.java new file mode 100644 index 00000000000..83889e16288 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/model/ExponentialBackoffProperties.java @@ -0,0 +1,30 @@ +package org.prebid.server.spring.config.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +@Data +@Validated +@NoArgsConstructor +public class ExponentialBackoffProperties { + + @NotNull + @Min(1) + private Integer delayMillis; + + @NotNull + @Min(1) + private Integer maxDelayMillis; + + @NotNull + @Min(0) + private Double factor; + + @NotNull + @Min(0) + private Double jitter; +} diff --git a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java index b0206149f0d..6e0373fba11 100644 --- a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java @@ -5,12 +5,12 @@ import lombok.Data; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.httpclient.HttpClient; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.time.Clock; @Validated diff --git a/src/main/java/org/prebid/server/spring/config/model/HttpClientCircuitBreakerProperties.java b/src/main/java/org/prebid/server/spring/config/model/HttpClientCircuitBreakerProperties.java index 26830122dda..9693e78bb93 100644 --- a/src/main/java/org/prebid/server/spring/config/model/HttpClientCircuitBreakerProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/HttpClientCircuitBreakerProperties.java @@ -5,8 +5,8 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; @Validated @Data diff --git a/src/main/java/org/prebid/server/spring/config/model/HttpClientProperties.java b/src/main/java/org/prebid/server/spring/config/model/HttpClientProperties.java index a2981e02849..06ec12bac87 100644 --- a/src/main/java/org/prebid/server/spring/config/model/HttpClientProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/HttpClientProperties.java @@ -4,8 +4,8 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; @Validated @Data diff --git a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java b/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java index a354dd46b3e..09e56ac59c6 100644 --- a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java @@ -4,9 +4,9 @@ import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; @Validated @Data @@ -22,14 +22,14 @@ public class RemoteFileSyncerProperties { @NotBlank private String tmpFilepath; - @NotNull @Min(1) private Integer retryCount; - @NotNull @Min(1) private Long retryIntervalMs; + private ExponentialBackoffProperties retry; + @NotNull @Min(1) private Long timeoutMs; diff --git a/src/main/java/org/prebid/server/spring/config/retry/ExponentialBackoffRetryPolicyConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/retry/ExponentialBackoffRetryPolicyConfigurationProperties.java index af6d94c490b..ca584979578 100644 --- a/src/main/java/org/prebid/server/spring/config/retry/ExponentialBackoffRetryPolicyConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/retry/ExponentialBackoffRetryPolicyConfigurationProperties.java @@ -4,8 +4,8 @@ import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; -import javax.validation.constraints.Positive; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; @Data @Validated diff --git a/src/main/java/org/prebid/server/spring/config/retry/FixedIntervalRetryPolicyConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/retry/FixedIntervalRetryPolicyConfigurationProperties.java index 332d6c5787c..bc55c9f6c03 100644 --- a/src/main/java/org/prebid/server/spring/config/retry/FixedIntervalRetryPolicyConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/retry/FixedIntervalRetryPolicyConfigurationProperties.java @@ -4,7 +4,7 @@ import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; import org.springframework.validation.annotation.Validated; -import javax.validation.constraints.Min; +import jakarta.validation.constraints.Min; @Data @Validated diff --git a/src/main/java/org/prebid/server/spring/config/server/HttpServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/HttpServerConfiguration.java deleted file mode 100644 index c14dcf9c408..00000000000 --- a/src/main/java/org/prebid/server/spring/config/server/HttpServerConfiguration.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.prebid.server.spring.config.server; - -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.Router; -import org.prebid.server.handler.ExceptionHandler; -import org.prebid.server.vertx.ContextRunner; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Configuration; - -import javax.annotation.PostConstruct; - -@Configuration -@ConditionalOnProperty(name = "server.http.enabled", havingValue = "true") -public class HttpServerConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(HttpServerConfiguration.class); - - @Autowired - private ContextRunner contextRunner; - - @Autowired - private Vertx vertx; - - @Autowired - private HttpServerOptions httpServerOptions; - - @Autowired - private ExceptionHandler exceptionHandler; - - @Autowired - @Qualifier("router") - private Router router; - - @Value("#{'${http.port:${server.http.port}}'}") - private Integer httpPort; - - // TODO: remove support for properties with http prefix after transition period - @Value("#{'${vertx.http-server-instances:${server.http.server-instances}}'}") - private Integer httpServerNum; - - @PostConstruct - public void startHttpServer() { - logger.info( - "Starting {0} instances of Http Server to serve requests on port {1,number,#}", - httpServerNum, - httpPort); - - contextRunner.runOnNewContext(httpServerNum, promise -> - vertx.createHttpServer(httpServerOptions) - .exceptionHandler(exceptionHandler) - .requestHandler(router) - .listen(httpPort, promise)); - - logger.info("Successfully started {0} instances of Http Server", httpServerNum); - } -} diff --git a/src/main/java/org/prebid/server/spring/config/server/UnixSocketServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/UnixSocketServerConfiguration.java deleted file mode 100644 index 54ae616f496..00000000000 --- a/src/main/java/org/prebid/server/spring/config/server/UnixSocketServerConfiguration.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.prebid.server.spring.config.server; - -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.core.net.SocketAddress; -import io.vertx.ext.web.Router; -import org.prebid.server.handler.ExceptionHandler; -import org.prebid.server.vertx.ContextRunner; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Configuration; - -import javax.annotation.PostConstruct; - -@Configuration -@ConditionalOnProperty(name = "server.unix-socket.enabled", havingValue = "true") -public class UnixSocketServerConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(UnixSocketServerConfiguration.class); - - @Autowired - private ContextRunner contextRunner; - - @Autowired - private Vertx vertx; - - @Autowired - private HttpServerOptions httpServerOptions; - - @Autowired - private ExceptionHandler exceptionHandler; - - @Autowired - @Qualifier("router") - private Router router; - - @Value("${server.unix-socket.path}") - private String socketPath; - - @Value("${server.unix-socket.server-instances}") - private Integer serverNum; - - @PostConstruct - public void startUnixSocketServer() { - logger.info( - "Starting {0} instances of Unix Socket Server to serve requests on socket {1}", - serverNum, - socketPath); - - contextRunner.runOnNewContext(serverNum, promise -> - vertx.createHttpServer(httpServerOptions) - .exceptionHandler(exceptionHandler) - .requestHandler(router) - .listen(SocketAddress.domainSocketAddress(socketPath), promise)); - - logger.info("Successfully started {0} instances of Unix Socket Server", serverNum); - } -} diff --git a/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java new file mode 100644 index 00000000000..31f50e8db6d --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/server/admin/AdminEndpointsConfiguration.java @@ -0,0 +1,219 @@ +package org.prebid.server.spring.config.server.admin; + +import com.codahale.metrics.MetricRegistry; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.handler.admin.AccountCacheInvalidationHandler; +import org.prebid.server.handler.admin.AdminResourceWrapper; +import org.prebid.server.handler.admin.CollectedMetricsHandler; +import org.prebid.server.handler.admin.CurrencyRatesHandler; +import org.prebid.server.handler.admin.HttpInteractionLogHandler; +import org.prebid.server.handler.admin.LoggerControlKnobHandler; +import org.prebid.server.handler.admin.SettingsCacheNotificationHandler; +import org.prebid.server.handler.admin.TracerLogHandler; +import org.prebid.server.handler.admin.VersionHandler; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.CriteriaManager; +import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.log.LoggerControlKnob; +import org.prebid.server.settings.CachingApplicationSettings; +import org.prebid.server.settings.SettingsCache; +import org.prebid.server.util.VersionInfo; +import org.prebid.server.vertx.verticles.server.admin.AdminResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +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 org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +@Configuration +public class AdminEndpointsConfiguration { + + @Bean + @ConditionalOnExpression("${admin-endpoints.version.enabled} == true") + AdminResource versionEndpoint( + VersionInfo versionInfo, + JacksonMapper mapper, + @Value("${admin-endpoints.version.path}") String path, + @Value("${admin-endpoints.version.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.version.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new VersionHandler(versionInfo.getVersion(), versionInfo.getCommitHash(), mapper, path)); + } + + @Bean + @ConditionalOnExpression("${currency-converter.external-rates.enabled} == true" + + " and ${admin-endpoints.currency-rates.enabled} == true") + AdminResource currencyConversionRatesEndpoint( + CurrencyConversionService currencyConversionRates, + JacksonMapper mapper, + @Value("${admin-endpoints.currency-rates.path}") String path, + @Value("${admin-endpoints.currency-rates.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.currency-rates.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new CurrencyRatesHandler(currencyConversionRates, path, mapper)); + } + + @Bean + @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" + + " and ${admin-endpoints.storedrequest.enabled} == true") + AdminResource cacheNotificationEndpoint( + SettingsCache settingsCache, + JacksonMapper mapper, + @Value("${admin-endpoints.storedrequest.path}") String path, + @Value("${admin-endpoints.storedrequest.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.storedrequest.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new SettingsCacheNotificationHandler(settingsCache, mapper, path)); + } + + @Bean + @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" + + " and ${admin-endpoints.storedrequest-amp.enabled} == true") + AdminResource ampCacheNotificationEndpoint( + SettingsCache ampSettingsCache, + JacksonMapper mapper, + @Value("${admin-endpoints.storedrequest-amp.path}") String path, + @Value("${admin-endpoints.storedrequest-amp.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.storedrequest-amp.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new SettingsCacheNotificationHandler(ampSettingsCache, mapper, path)); + } + + @Bean + @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" + + " and ${admin-endpoints.cache-invalidation.enabled} == true") + AdminResource cacheInvalidateNotificationEndpoint( + CachingApplicationSettings cachingApplicationSettings, + @Value("${admin-endpoints.cache-invalidation.path}") String path, + @Value("${admin-endpoints.cache-invalidation.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.cache-invalidation.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new AccountCacheInvalidationHandler(cachingApplicationSettings, path)); + } + + @Bean + @ConditionalOnExpression("${admin-endpoints.logging-httpinteraction.enabled} == true") + AdminResource loggingHttpInteractionEndpoint( + @Value("${logging.http-interaction.max-limit}") int maxLimit, + HttpInteractionLogger httpInteractionLogger, + @Value("${admin-endpoints.logging-httpinteraction.path}") String path, + @Value("${admin-endpoints.logging-httpinteraction.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.logging-httpinteraction.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new HttpInteractionLogHandler(maxLimit, httpInteractionLogger, path)); + } + + @Bean + @ConditionalOnExpression("${admin-endpoints.logging-changelevel.enabled} == true") + AdminResource loggingChangeLevelEndpoint( + @Value("${logging.change-level.max-duration-ms}") long maxDuration, + LoggerControlKnob loggerControlKnob, + @Value("${admin-endpoints.logging-changelevel.path}") String path, + @Value("${admin-endpoints.logging-changelevel.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.logging-changelevel.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new LoggerControlKnobHandler(maxDuration, loggerControlKnob, path)); + } + + @Bean + @ConditionalOnProperty(prefix = "admin-endpoints.tracelog", name = "enabled", havingValue = "true") + AdminResource tracerLogEndpoint( + CriteriaManager criteriaManager, + @Value("${admin-endpoints.tracelog.path}") String path, + @Value("${admin-endpoints.tracelog.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.tracelog.protected}") boolean isProtected) { + + return new AdminResourceWrapper(path, isOnApplicationPort, isProtected, new TracerLogHandler(criteriaManager)); + } + + @Bean + @ConditionalOnExpression("${admin-endpoints.collected-metrics.enabled} == true") + AdminResource collectedMetricsAdminEndpoint( + MetricRegistry metricRegistry, + JacksonMapper mapper, + @Value("${admin-endpoints.collected-metrics.path}") String path, + @Value("${admin-endpoints.collected-metrics.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.collected-metrics.protected}") boolean isProtected) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new CollectedMetricsHandler(metricRegistry, mapper, path)); + } + + @Bean + AdminResourcesBinder applicationPortAdminResourcesBinder(Map adminEndpointCredentials, + List resources) { + + final List applicationPortAdminResources = resources.stream() + .filter(AdminResource::isOnApplicationPort) + .toList(); + + return new AdminResourcesBinder(adminEndpointCredentials, applicationPortAdminResources); + } + + @Bean + AdminResourcesBinder adminPortAdminResourcesBinder(Map adminEndpointCredentials, + List resources) { + + final List adminPortAdminResources = resources.stream() + .filter(Predicate.not(AdminResource::isOnApplicationPort)) + .toList(); + + return new AdminResourcesBinder(adminEndpointCredentials, adminPortAdminResources); + } + + @Bean + Map adminEndpointCredentials(@Autowired(required = false) AdminEndpointCredentials credentials) { + return ObjectUtils.defaultIfNull(credentials.getCredentials(), Collections.emptyMap()); + } + + @Component + @ConfigurationProperties(prefix = "admin-endpoints") + @Data + @NoArgsConstructor + public static class AdminEndpointCredentials { + + private Map credentials; + } +} diff --git a/src/main/java/org/prebid/server/spring/config/server/admin/AdminResourcesBinder.java b/src/main/java/org/prebid/server/spring/config/server/admin/AdminResourcesBinder.java new file mode 100644 index 00000000000..41b695bb2c6 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/server/admin/AdminResourcesBinder.java @@ -0,0 +1,50 @@ +package org.prebid.server.spring.config.server.admin; + +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.AuthenticationHandler; +import io.vertx.ext.web.handler.BasicAuthHandler; +import org.prebid.server.vertx.verticles.server.admin.AdminResource; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class AdminResourcesBinder { + + private final Map credentials; + private final List resources; + + public AdminResourcesBinder(Map credentials, List resources) { + this.credentials = credentials; + this.resources = Objects.requireNonNull(resources); + } + + public void bind(Router router) { + for (AdminResource resource : resources) { + router + .route(resource.path()) + .handler(resource.isSecured() ? securedAuthHandler() : PassNextHandler.INSTANCE) + .handler(resource); + } + } + + private AuthenticationHandler securedAuthHandler() { + if (credentials == null) { + throw new IllegalArgumentException("Credentials for admin endpoint is empty."); + } + + return BasicAuthHandler.create(new AdminServerAuthProvider(credentials)); + } + + private static class PassNextHandler implements Handler { + + private static final Handler INSTANCE = new PassNextHandler(); + + @Override + public void handle(RoutingContext event) { + event.next(); + } + } +} diff --git a/src/main/java/org/prebid/server/spring/config/server/admin/AdminServerAuthProvider.java b/src/main/java/org/prebid/server/spring/config/server/admin/AdminServerAuthProvider.java new file mode 100644 index 00000000000..f95980d1a60 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/server/admin/AdminServerAuthProvider.java @@ -0,0 +1,39 @@ +package org.prebid.server.spring.config.server.admin; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.AuthProvider; +import io.vertx.ext.auth.User; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; + +public class AdminServerAuthProvider implements AuthProvider { + + private final Map credentials; + + public AdminServerAuthProvider(Map credentials) { + this.credentials = credentials; + } + + @Override + public void authenticate(JsonObject authInfo, Handler> resultHandler) { + if (MapUtils.isEmpty(credentials)) { + resultHandler.handle(Future.failedFuture("Credentials not set in configuration.")); + return; + } + + final String requestUsername = authInfo.getString("username"); + final String requestPassword = StringUtils.chomp(authInfo.getString("password")); + + final String storedPassword = credentials.get(requestUsername); + if (StringUtils.isNotBlank(requestPassword) && StringUtils.equals(storedPassword, requestPassword)) { + resultHandler.handle(Future.succeededFuture()); + } else { + resultHandler.handle(Future.failedFuture("No such user, or password incorrect.")); + } + } +} diff --git a/src/main/java/org/prebid/server/spring/config/server/admin/AdminServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/admin/AdminServerConfiguration.java new file mode 100644 index 00000000000..e88e04be03d --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/server/admin/AdminServerConfiguration.java @@ -0,0 +1,40 @@ +package org.prebid.server.spring.config.server.admin; + +import io.vertx.core.Vertx; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import org.prebid.server.vertx.verticles.VerticleDefinition; +import org.prebid.server.vertx.verticles.server.ServerVerticle; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(prefix = "admin", name = "port") +public class AdminServerConfiguration { + + @Bean + Router adminPortAdminServerRouter(Vertx vertx, + AdminResourcesBinder adminPortAdminResourcesBinder, + BodyHandler bodyHandler) { + + final Router router = Router.router(vertx); + router.route().handler(bodyHandler); + + adminPortAdminResourcesBinder.bind(router); + return router; + } + + @Bean + VerticleDefinition adminPortAdminHttpServerVerticleDefinition(Router adminPortAdminServerRouter, + @Value("${admin.port}") int port) { + + return VerticleDefinition.ofSingleInstance( + () -> new ServerVerticle( + "Admin Http Server", + SocketAddress.inetSocketAddress(port, "0.0.0.0"), + adminPortAdminServerRouter)); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java new file mode 100644 index 00000000000..82794d58ffb --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java @@ -0,0 +1,453 @@ +package org.prebid.server.spring.config.server.application; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.CorsHandler; +import io.vertx.ext.web.handler.StaticHandler; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AmpResponsePostProcessor; +import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.SkippedAuctionService; +import org.prebid.server.auction.VideoResponseFactory; +import org.prebid.server.auction.gpp.CookieSyncGppService; +import org.prebid.server.auction.gpp.SetuidGppService; +import org.prebid.server.auction.privacy.contextfactory.CookieSyncPrivacyContextFactory; +import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory; +import org.prebid.server.auction.requestfactory.AmpRequestFactory; +import org.prebid.server.auction.requestfactory.AuctionRequestFactory; +import org.prebid.server.auction.requestfactory.VideoRequestFactory; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cache.CoreCacheService; +import org.prebid.server.cookie.CookieDeprecationService; +import org.prebid.server.cookie.CookieSyncService; +import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.handler.BidderParamHandler; +import org.prebid.server.handler.CookieSyncHandler; +import org.prebid.server.handler.ExceptionHandler; +import org.prebid.server.handler.GetuidsHandler; +import org.prebid.server.handler.NoCacheHandler; +import org.prebid.server.handler.NotificationEventHandler; +import org.prebid.server.handler.OptoutHandler; +import org.prebid.server.handler.SetuidHandler; +import org.prebid.server.handler.StatusHandler; +import org.prebid.server.handler.VtrackHandler; +import org.prebid.server.handler.info.BidderDetailsHandler; +import org.prebid.server.handler.info.BiddersHandler; +import org.prebid.server.handler.info.filters.BaseOnlyBidderInfoFilterStrategy; +import org.prebid.server.handler.info.filters.BidderInfoFilterStrategy; +import org.prebid.server.handler.info.filters.EnabledOnlyBidderInfoFilterStrategy; +import org.prebid.server.handler.openrtb2.AmpHandler; +import org.prebid.server.handler.openrtb2.VideoHandler; +import org.prebid.server.health.HealthChecker; +import org.prebid.server.health.PeriodicHealthChecker; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.HttpInteractionLogger; +import org.prebid.server.metric.Metrics; +import org.prebid.server.optout.GoogleRecaptchaVerifier; +import org.prebid.server.privacy.HostVendorTcfDefinerService; +import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.spring.config.server.admin.AdminResourcesBinder; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.validation.BidderParamValidator; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.verticles.VerticleDefinition; +import org.prebid.server.vertx.verticles.server.ServerVerticle; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; +import org.springframework.beans.factory.annotation.Value; +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 org.springframework.stereotype.Component; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Configuration +public class ApplicationServerConfiguration { + + @Value("${logging.sampling-rate:0.01}") + private double logSamplingRate; + + @Bean + @ConditionalOnProperty(name = "server.http.enabled", havingValue = "true") + VerticleDefinition httpApplicationServerVerticleDefinition( + HttpServerOptions httpServerOptions, + @Value("${server.http.port}") int port, + Router applicationServerRouter, + ExceptionHandler exceptionHandler, + @Value("${server.http.server-instances}") int instances) { + + return VerticleDefinition.ofMultiInstance( + () -> new ServerVerticle( + "Application Http Server", + httpServerOptions, + SocketAddress.inetSocketAddress(port, "0.0.0.0"), + applicationServerRouter, + exceptionHandler), + instances); + } + + @Bean + @ConditionalOnProperty(name = "server.unix-socket.enabled", havingValue = "true") + VerticleDefinition unixSocketApplicationServerVerticleDefinition( + HttpServerOptions httpServerOptions, + @Value("${server.unix-socket.path}") String path, + Router applicationServerRouter, + ExceptionHandler exceptionHandler, + @Value("${server.unix-socket.server-instances}") Integer instances) { + + return VerticleDefinition.ofMultiInstance( + () -> new ServerVerticle( + "Application Unix Socket Server", + httpServerOptions, + SocketAddress.domainSocketAddress(path), + applicationServerRouter, + exceptionHandler), + instances); + } + + @Bean + HttpServerOptions httpServerOptions( + @Value("${server.max-headers-size}") int maxHeaderSize, + @Value("${server.max-initial-line-length}") int maxInitialLineLength, + @Value("${server.ssl}") boolean ssl, + @Value("${server.jks-path}") String jksPath, + @Value("${server.jks-password}") String jksPassword, + @Value("${server.idle-timeout}") int idleTimeout, + @Value("${server.enable-quickack:#{null}}") Optional enableQuickAck, + @Value("${server.enable-reuseport:#{null}}") Optional enableReusePort) { + + final HttpServerOptions httpServerOptions = new HttpServerOptions() + .setHandle100ContinueAutomatically(true) + .setMaxInitialLineLength(maxInitialLineLength) + .setMaxHeaderSize(maxHeaderSize) + .setCompressionSupported(true) + .setDecompressionSupported(true) + .setIdleTimeout(idleTimeout); // kick off long processing requests, value in seconds + enableQuickAck.ifPresent(httpServerOptions::setTcpQuickAck); + enableReusePort.ifPresent(httpServerOptions::setReusePort); + if (ssl) { + final JksOptions jksOptions = new JksOptions() + .setPath(jksPath) + .setPassword(jksPassword); + + httpServerOptions + .setSsl(true) + .setKeyStoreOptions(jksOptions); + } + + return httpServerOptions; + } + + @Bean + ExceptionHandler exceptionHandler(Metrics metrics) { + return ExceptionHandler.create(metrics); + } + + @Bean + Router applicationServerRouter(Vertx vertx, + BodyHandler bodyHandler, + NoCacheHandler noCacheHandler, + CorsHandler corsHandler, + List resources, + AdminResourcesBinder applicationPortAdminResourcesBinder, + StaticHandler staticHandler) { + + final Router router = Router.router(vertx); + router.route().handler(bodyHandler); + router.route().handler(noCacheHandler); + router.route().handler(corsHandler); + + resources.forEach(resource -> + resource.endpoints().forEach(endpoint -> + router.route(endpoint.getMethod(), endpoint.getPath()).handler(resource))); + + applicationPortAdminResourcesBinder.bind(router); + + router.get("/static/*").handler(staticHandler); + router.get("/").handler(staticHandler); // serves index.html by default + + return router; + } + + @Bean + NoCacheHandler noCacheHandler() { + return NoCacheHandler.create(); + } + + @Bean + CorsHandler corsHandler() { + return CorsHandler.create(".*") + .allowCredentials(true) + .allowedHeaders(new HashSet<>(Arrays.asList( + HttpUtil.ORIGIN_HEADER.toString(), + HttpUtil.ACCEPT_HEADER.toString(), + HttpUtil.CONTENT_TYPE_HEADER.toString(), + HttpUtil.X_REQUESTED_WITH_HEADER.toString()))) + .allowedMethods(new HashSet<>(Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.HEAD, + HttpMethod.OPTIONS))); + } + + @Bean + org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler( + ExchangeService exchangeService, + SkippedAuctionService skippedAuctionService, + AuctionRequestFactory auctionRequestFactory, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + Clock clock, + HttpInteractionLogger httpInteractionLogger, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return new org.prebid.server.handler.openrtb2.AuctionHandler( + logSamplingRate, + auctionRequestFactory, + exchangeService, + skippedAuctionService, + analyticsReporter, + metrics, + clock, + httpInteractionLogger, + prebidVersionProvider, + mapper); + } + + @Bean + AmpHandler openrtbAmpHandler( + AmpRequestFactory ampRequestFactory, + ExchangeService exchangeService, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + Clock clock, + BidderCatalog bidderCatalog, + AmpProperties ampProperties, + AmpResponsePostProcessor ampResponsePostProcessor, + HttpInteractionLogger httpInteractionLogger, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return new AmpHandler( + ampRequestFactory, + exchangeService, + analyticsReporter, + metrics, + clock, + bidderCatalog, + ampProperties.getCustomTargetingSet(), + ampResponsePostProcessor, + httpInteractionLogger, + prebidVersionProvider, + mapper, + logSamplingRate); + } + + @Bean + VideoHandler openrtbVideoHandler( + VideoRequestFactory videoRequestFactory, + VideoResponseFactory videoResponseFactory, + ExchangeService exchangeService, + CoreCacheService coreCacheService, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + Clock clock, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return new VideoHandler( + videoRequestFactory, + videoResponseFactory, + exchangeService, + coreCacheService, analyticsReporter, + metrics, + clock, + prebidVersionProvider, + mapper); + } + + @Bean + StatusHandler statusHandler(List healthCheckers, JacksonMapper mapper) { + healthCheckers.stream() + .filter(PeriodicHealthChecker.class::isInstance) + .map(PeriodicHealthChecker.class::cast) + .forEach(PeriodicHealthChecker::initialize); + return new StatusHandler(healthCheckers, mapper); + } + + @Bean + CookieSyncHandler cookieSyncHandler( + @Value("${cookie-sync.default-timeout-ms}") int defaultTimeoutMs, + UidsCookieService uidsCookieService, + CookieSyncGppService cookieSyncGppProcessor, + CookieDeprecationService cookieDeprecationService, + ActivityInfrastructureCreator activityInfrastructureCreator, + ApplicationSettings applicationSettings, + CookieSyncService cookieSyncService, + CookieSyncPrivacyContextFactory cookieSyncPrivacyContextFactory, + AnalyticsReporterDelegator analyticsReporterDelegator, + Metrics metrics, + TimeoutFactory timeoutFactory, + JacksonMapper mapper) { + + return new CookieSyncHandler( + defaultTimeoutMs, + logSamplingRate, + uidsCookieService, + cookieDeprecationService, + cookieSyncGppProcessor, + activityInfrastructureCreator, + cookieSyncService, + applicationSettings, + cookieSyncPrivacyContextFactory, + analyticsReporterDelegator, + metrics, + timeoutFactory, + mapper); + } + + @Bean + SetuidHandler setuidHandler( + @Value("${setuid.default-timeout-ms}") int defaultTimeoutMs, + UidsCookieService uidsCookieService, + ApplicationSettings applicationSettings, + BidderCatalog bidderCatalog, + SetuidPrivacyContextFactory setuidPrivacyContextFactory, + SetuidGppService setuidGppService, + ActivityInfrastructureCreator activityInfrastructureCreator, + HostVendorTcfDefinerService tcfDefinerService, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + TimeoutFactory timeoutFactory) { + + return new SetuidHandler( + defaultTimeoutMs, + uidsCookieService, + applicationSettings, + bidderCatalog, + setuidPrivacyContextFactory, + setuidGppService, + activityInfrastructureCreator, + tcfDefinerService, + analyticsReporter, + metrics, + timeoutFactory); + } + + @Bean + GetuidsHandler getuidsHandler(UidsCookieService uidsCookieService, JacksonMapper mapper) { + return new GetuidsHandler(uidsCookieService, mapper); + } + + @Bean + VtrackHandler vtrackHandler( + @Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs, + @Value("${vtrack.allow-unknown-bidder}") boolean allowUnknownBidder, + @Value("${vtrack.modify-vast-for-unknown-bidder}") boolean modifyVastForUnknownBidder, + ApplicationSettings applicationSettings, + BidderCatalog bidderCatalog, + CoreCacheService coreCacheService, + TimeoutFactory timeoutFactory, + JacksonMapper mapper) { + + return new VtrackHandler( + defaultTimeoutMs, + allowUnknownBidder, + modifyVastForUnknownBidder, + applicationSettings, + bidderCatalog, + coreCacheService, + timeoutFactory, + mapper); + } + + @Bean + OptoutHandler optoutHandler( + @Value("${external-url}") String externalUrl, + @Value("${host-cookie.opt-out-url}") String optoutUrl, + @Value("${host-cookie.opt-in-url}") String optinUrl, + GoogleRecaptchaVerifier googleRecaptchaVerifier, + UidsCookieService uidsCookieService) { + + return new OptoutHandler( + googleRecaptchaVerifier, + uidsCookieService, + OptoutHandler.getOptoutRedirectUrl(externalUrl), + HttpUtil.validateUrl(optoutUrl), + HttpUtil.validateUrl(optinUrl)); + } + + @Bean + BidderParamHandler bidderParamHandler(BidderParamValidator bidderParamValidator) { + return new BidderParamHandler(bidderParamValidator); + } + + @Bean + BidderInfoFilterStrategy enabledOnlyBidderInfoFilterStrategy(BidderCatalog bidderCatalog) { + return new EnabledOnlyBidderInfoFilterStrategy(bidderCatalog); + } + + @Bean + BidderInfoFilterStrategy baseOnlyBidderInfoFilterStrategy(BidderCatalog bidderCatalog) { + return new BaseOnlyBidderInfoFilterStrategy(bidderCatalog); + } + + @Bean + BiddersHandler biddersHandler(BidderCatalog bidderCatalog, + List filterStrategies, + JacksonMapper mapper) { + return new BiddersHandler(bidderCatalog, filterStrategies, mapper); + } + + @Bean + BidderDetailsHandler bidderDetailsHandler(BidderCatalog bidderCatalog, JacksonMapper mapper) { + return new BidderDetailsHandler(bidderCatalog, mapper); + } + + @Bean + NotificationEventHandler notificationEventHandler(ActivityInfrastructureCreator activityInfrastructureCreator, + AnalyticsReporterDelegator analyticsReporterDelegator, + TimeoutFactory timeoutFactory, + ApplicationSettings applicationSettings, + @Value("${event.default-timeout-ms}") long defaultTimeoutMillis) { + + return new NotificationEventHandler( + activityInfrastructureCreator, + analyticsReporterDelegator, + timeoutFactory, + applicationSettings, + defaultTimeoutMillis); + } + + @Bean + StaticHandler staticHandler() { + return StaticHandler.create("static").setCachingEnabled(false); + } + + @Component + @ConfigurationProperties(prefix = "amp") + @Data + @NoArgsConstructor + private static class AmpProperties { + + private List customTargeting = new ArrayList<>(); + + Set getCustomTargetingSet() { + return new HashSet<>(customTargeting); + } + } +} diff --git a/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java b/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java index 6e599602b9e..fcff6e0a397 100644 --- a/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java +++ b/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java @@ -7,8 +7,8 @@ import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertySourceFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Properties; diff --git a/src/main/java/org/prebid/server/util/BidderUtil.java b/src/main/java/org/prebid/server/util/BidderUtil.java index f156628caae..d6869e3b72d 100644 --- a/src/main/java/org/prebid/server/util/BidderUtil.java +++ b/src/main/java/org/prebid/server/util/BidderUtil.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -111,20 +110,21 @@ public static boolean isNullOrZero(Integer value) { } public static BidType getBidType(Bid bid, Map impIdToImpMap) { - return Optional.ofNullable(impIdToImpMap.get(bid.getImpid())) - .map(imp -> { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getXNative() != null) { - return BidType.xNative; - } else if (imp.getAudio() != null) { - return BidType.audio; - } else { - return BidType.banner; - } - }) - .orElse(BidType.banner); + final Imp imp = impIdToImpMap.get(bid.getImpid()); + if (imp == null) { + return BidType.banner; + } + + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getAudio() != null) { + return BidType.audio; + } else { + return BidType.banner; + } } } diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java index 767d326b0a7..ad9dd8a9238 100644 --- a/src/main/java/org/prebid/server/util/HttpUtil.java +++ b/src/main/java/org/prebid/server/util/HttpUtil.java @@ -6,13 +6,11 @@ import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.exception.PreBidException; import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; @@ -21,15 +19,12 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.time.ZonedDateTime; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -47,7 +42,6 @@ public final class HttpUtil { public static final CharSequence X_FORWARDED_FOR_HEADER = HttpHeaders.createOptimized("X-Forwarded-For"); public static final CharSequence X_REAL_IP_HEADER = HttpHeaders.createOptimized("X-Real-Ip"); public static final CharSequence DNT_HEADER = HttpHeaders.createOptimized("DNT"); - public static final CharSequence X_REQUEST_AGENT_HEADER = HttpHeaders.createOptimized("X-Request-Agent"); public static final CharSequence ORIGIN_HEADER = HttpHeaders.createOptimized("Origin"); public static final CharSequence ACCEPT_HEADER = HttpHeaders.createOptimized("Accept"); public static final CharSequence SEC_GPC_HEADER = HttpHeaders.createOptimized("Sec-GPC"); @@ -65,27 +59,24 @@ public final class HttpUtil { public static final CharSequence ACCEPT_LANGUAGE_HEADER = HttpHeaders.createOptimized("Accept-Language"); public static final CharSequence SET_COOKIE_HEADER = HttpHeaders.createOptimized("Set-Cookie"); public static final CharSequence AUTHORIZATION_HEADER = HttpHeaders.createOptimized("Authorization"); - public static final CharSequence DATE_HEADER = HttpHeaders.createOptimized("Date"); public static final CharSequence CACHE_CONTROL_HEADER = HttpHeaders.createOptimized("Cache-Control"); public static final CharSequence EXPIRES_HEADER = HttpHeaders.createOptimized("Expires"); public static final CharSequence PRAGMA_HEADER = HttpHeaders.createOptimized("Pragma"); public static final CharSequence LOCATION_HEADER = HttpHeaders.createOptimized("Location"); public static final CharSequence CONNECTION_HEADER = HttpHeaders.createOptimized("Connection"); - public static final CharSequence ACCEPT_ENCODING_HEADER = HttpHeaders.createOptimized("Accept-Encoding"); public static final CharSequence CONTENT_ENCODING_HEADER = HttpHeaders.createOptimized("Content-Encoding"); public static final CharSequence X_OPENRTB_VERSION_HEADER = HttpHeaders.createOptimized("x-openrtb-version"); public static final CharSequence X_PREBID_HEADER = HttpHeaders.createOptimized("x-prebid"); + public static final CharSequence X_PBC_API_KEY_HEADER = HttpHeaders.createOptimized("x-pbc-api-key"); private static final Set SENSITIVE_HEADERS = Set.of(AUTHORIZATION_HEADER.toString()); - public static final CharSequence PG_TRX_ID = HttpHeaders.createOptimized("pg-trx-id"); - public static final CharSequence PG_IGNORE_PACING = HttpHeaders.createOptimized("X-Prebid-PG-ignore-pacing"); //the low-entropy client hints public static final CharSequence SAVE_DATA = HttpHeaders.createOptimized("Save-Data"); public static final CharSequence SEC_CH_UA = HttpHeaders.createOptimized("Sec-CH-UA"); public static final CharSequence SEC_CH_UA_MOBILE = HttpHeaders.createOptimized("Sec-CH-UA-Mobile"); public static final CharSequence SEC_CH_UA_PLATFORM = HttpHeaders.createOptimized("Sec-CH-UA-Platform"); - - private static final String BASIC_AUTH_PATTERN = "Basic %s"; + public static final String MACROS_OPEN = "{{"; + public static final String MACROS_CLOSE = "}}"; private HttpUtil() { } @@ -94,6 +85,10 @@ private HttpUtil() { * Checks the input string for using as URL. */ public static String validateUrl(String url) { + if (containsMacrosses(url)) { + return url; + } + try { return new URL(url).toString(); } catch (MalformedURLException e) { @@ -101,6 +96,11 @@ public static String validateUrl(String url) { } } + // TODO: We need our own way to work with url macrosses + private static boolean containsMacrosses(String url) { + return StringUtils.contains(url, MACROS_OPEN) && StringUtils.contains(url, MACROS_CLOSE); + } + /** * Returns encoded URL for the given value. *

@@ -138,28 +138,6 @@ public static void addHeaderIfValueIsNotEmpty(MultiMap headers, CharSequence hea } } - public static ZonedDateTime getDateFromHeader(MultiMap headers, String header) { - return getDateFromHeader(headers::get, header); - } - - public static ZonedDateTime getDateFromHeader(CaseInsensitiveMultiMap headers, String header) { - return getDateFromHeader(headers::get, header); - } - - private static ZonedDateTime getDateFromHeader(Function headerGetter, String header) { - final String isoTimeStamp = headerGetter.apply(header); - if (isoTimeStamp == null) { - return null; - } - - try { - return ZonedDateTime.parse(isoTimeStamp); - } catch (Exception e) { - throw new PreBidException( - "%s header is not compatible to ISO-8601 format: %s".formatted(header, isoTimeStamp)); - } - } - public static String getHostFromUrl(String url) { if (StringUtils.isBlank(url)) { return null; @@ -196,20 +174,22 @@ public static String createCookiesHeader(RoutingContext routingContext) { .collect(Collectors.joining("; ")); } - public static boolean executeSafely(RoutingContext routingContext, Endpoint endpoint, + public static boolean executeSafely(RoutingContext routingContext, + Endpoint endpoint, Consumer responseConsumer) { + return executeSafely(routingContext, endpoint.value(), responseConsumer); } - public static boolean executeSafely(RoutingContext routingContext, String endpoint, + public static boolean executeSafely(RoutingContext routingContext, + String endpoint, Consumer responseConsumer) { final HttpServerResponse response = routingContext.response(); if (response.closed()) { - conditionalLogger.warn( - "Client already closed connection, response to %s will be skipped".formatted(endpoint), - 0.01); + conditionalLogger + .warn("Client already closed connection, response to %s will be skipped".formatted(endpoint), 0.01); return false; } @@ -217,19 +197,11 @@ public static boolean executeSafely(RoutingContext routingContext, String endpoi responseConsumer.accept(response); return true; } catch (Exception e) { - logger.warn("Failed to send {0} response: {1}", endpoint, e.getMessage()); + logger.warn("Failed to send {} response: {}", endpoint, e.getMessage()); return false; } } - /** - * Creates standart basic auth header value - */ - public static String makeBasicAuthHeaderValue(String username, String password) { - return BASIC_AUTH_PATTERN - .formatted(Base64.getEncoder().encodeToString((username + ':' + password).getBytes())); - } - /** * Converts {@link MultiMap} headers format to Map, where keys are headers names and values are lists * of header's values diff --git a/src/main/java/org/prebid/server/util/LineItemUtil.java b/src/main/java/org/prebid/server/util/LineItemUtil.java deleted file mode 100644 index 37d89040e3c..00000000000 --- a/src/main/java/org/prebid/server/util/LineItemUtil.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.prebid.server.util; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Deal; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Pmp; -import com.iab.openrtb.response.Bid; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtDeal; -import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; - -import java.util.List; -import java.util.Objects; - -public class LineItemUtil { - - private static final Logger logger = LoggerFactory.getLogger(LineItemUtil.class); - - private LineItemUtil() { - } - - /** - * Extracts line item ID from the given {@link Bid}. - */ - public static String lineItemIdFrom(Bid bid, List imps, JacksonMapper mapper) { - if (StringUtils.isEmpty(bid.getDealid())) { - return null; - } - final ExtDealLine extDealLine = extDealLineFrom(bid, imps, mapper); - return extDealLine != null ? extDealLine.getLineItemId() : null; - } - - private static ExtDealLine extDealLineFrom(Bid bid, List imps, JacksonMapper mapper) { - final Imp correspondingImp = imps.stream() - .filter(imp -> Objects.equals(imp.getId(), bid.getImpid())) - .findFirst() - .orElse(null); - return correspondingImp != null ? extDealLineFrom(bid, correspondingImp, mapper) : null; - } - - public static ExtDealLine extDealLineFrom(Bid bid, Imp imp, JacksonMapper mapper) { - if (StringUtils.isEmpty(bid.getDealid())) { - return null; - } - - final Pmp pmp = imp.getPmp(); - final List deals = pmp != null ? pmp.getDeals() : null; - return CollectionUtils.isEmpty(deals) - ? null - : deals.stream() - .filter(Objects::nonNull) - .filter(deal -> Objects.equals(deal.getId(), bid.getDealid())) // find deal by ID - .map(Deal::getExt) - .filter(Objects::nonNull) - .map((ObjectNode ext) -> dealExt(ext, mapper)) - .filter(Objects::nonNull) - .map(ExtDeal::getLine) - .findFirst() - .orElse(null); - } - - private static ExtDeal dealExt(JsonNode ext, JacksonMapper mapper) { - try { - return mapper.mapper().treeToValue(ext, ExtDeal.class); - } catch (JsonProcessingException e) { - logger.warn("Error decoding deal.ext: {0}", e, e.getMessage()); - return null; - } - } -} diff --git a/src/main/java/org/prebid/server/util/ListUtil.java b/src/main/java/org/prebid/server/util/ListUtil.java index e31aaa453ad..66efeaa1858 100644 --- a/src/main/java/org/prebid/server/util/ListUtil.java +++ b/src/main/java/org/prebid/server/util/ListUtil.java @@ -1,5 +1,6 @@ package org.prebid.server.util; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.util.algorithms.ListsUnionView; import java.util.List; @@ -12,4 +13,8 @@ private ListUtil() { public static List union(List first, List second) { return new ListsUnionView<>(first, second); } + + public static List nullIfEmpty(List value) { + return CollectionUtils.isEmpty(value) ? null : value; + } } diff --git a/src/main/java/org/prebid/server/util/PbsUtil.java b/src/main/java/org/prebid/server/util/PbsUtil.java new file mode 100644 index 00000000000..bc81f1ed2b5 --- /dev/null +++ b/src/main/java/org/prebid/server/util/PbsUtil.java @@ -0,0 +1,16 @@ +package org.prebid.server.util; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +public class PbsUtil { + + private PbsUtil() { + } + + public static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + return requestExt != null ? requestExt.getPrebid() : null; + } +} diff --git a/src/main/java/org/prebid/server/util/VersionInfo.java b/src/main/java/org/prebid/server/util/VersionInfo.java index b163be55f3c..f7240ea38bb 100644 --- a/src/main/java/org/prebid/server/util/VersionInfo.java +++ b/src/main/java/org/prebid/server/util/VersionInfo.java @@ -1,11 +1,11 @@ package org.prebid.server.util; import com.fasterxml.jackson.annotation.JsonProperty; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import java.io.IOException; import java.util.regex.Matcher; @@ -31,7 +31,7 @@ public static VersionInfo create(String revisionFilePath, JacksonMapper jacksonM revision = jacksonMapper.mapper().readValue(ResourceUtil.readFromClasspath(revisionFilePath), Revision.class); } catch (IllegalArgumentException | IOException e) { - logger.error("Was not able to read revision file {0}. Reason: {1}", revisionFilePath, e.getMessage()); + logger.error("Was not able to read revision file {}. Reason: {}", revisionFilePath, e.getMessage()); return new VersionInfo(UNDEFINED, UNDEFINED); } final String pbsVersion = revision.getPbsVersion(); diff --git a/src/main/java/org/prebid/server/util/system/CpuLoadAverageStats.java b/src/main/java/org/prebid/server/util/system/CpuLoadAverageStats.java index 43ac4177b91..6be897cd1d4 100644 --- a/src/main/java/org/prebid/server/util/system/CpuLoadAverageStats.java +++ b/src/main/java/org/prebid/server/util/system/CpuLoadAverageStats.java @@ -1,5 +1,6 @@ package org.prebid.server.util.system; +import io.vertx.core.Promise; import io.vertx.core.Vertx; import org.prebid.server.vertx.Initializable; import oshi.SystemInfo; @@ -30,9 +31,10 @@ public CpuLoadAverageStats(Vertx vertx, long measurementIntervalMillis) { } @Override - public void initialize() { + public void initialize(Promise initializePromise) { measureCpuLoad(); vertx.setPeriodic(measurementIntervalMillis, timerId -> measureCpuLoad()); + initializePromise.tryComplete(); } private void measureCpuLoad() { diff --git a/src/main/java/org/prebid/server/validation/ImpValidator.java b/src/main/java/org/prebid/server/validation/ImpValidator.java new file mode 100644 index 00000000000..d5d9ecba2f1 --- /dev/null +++ b/src/main/java/org/prebid/server/validation/ImpValidator.java @@ -0,0 +1,656 @@ +package org.prebid.server.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Asset; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.DataObject; +import com.iab.openrtb.request.EventTracker; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.ImageObject; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Metric; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Request; +import com.iab.openrtb.request.TitleObject; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.request.VideoObject; +import com.iab.openrtb.request.ntv.ContextSubType; +import com.iab.openrtb.request.ntv.ContextType; +import com.iab.openrtb.request.ntv.DataAssetType; +import com.iab.openrtb.request.ntv.EventTrackingMethod; +import com.iab.openrtb.request.ntv.EventType; +import com.iab.openrtb.request.ntv.PlacementType; +import com.iab.openrtb.request.ntv.Protocol; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; +import org.prebid.server.util.StreamUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +public class ImpValidator { + + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500; + + private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; + private static final String IMP_EXT = "imp"; + + private final BidderParamValidator bidderParamValidator; + private final BidderCatalog bidderCatalog; + private final JacksonMapper mapper; + + public ImpValidator(BidderParamValidator bidderParamValidator, BidderCatalog bidderCatalog, JacksonMapper mapper) { + this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.mapper = Objects.requireNonNull(mapper); + } + + public void validateImps(List imps, + Map aliases, + List warnings) throws ValidationException { + + for (int i = 0; i < imps.size(); i++) { + final Imp imp = imps.get(i); + validateImp(imp, "request.imp[%d]".formatted(i)); + fillAndValidateNative(imp.getXNative(), i); + validateImpExt(imp.getExt(), aliases, i, warnings); + } + } + + public void validateImp(Imp imp) throws ValidationException { + validateImp(imp, "imp[id=%s]".formatted(imp.getId())); + } + + private void validateImp(Imp imp, String msgPrefix) throws ValidationException { + if (StringUtils.isBlank(imp.getId())) { + throw new ValidationException("%s missing required field: \"id\"", msgPrefix); + } + if (imp.getMetric() != null && !imp.getMetric().isEmpty()) { + validateMetrics(imp.getMetric(), msgPrefix); + } + if (imp.getBanner() == null && imp.getVideo() == null && imp.getAudio() == null && imp.getXNative() == null) { + throw new ValidationException( + "%s must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", + msgPrefix); + } + + final boolean isInterstitialImp = Objects.equals(imp.getInstl(), 1); + validateBanner(imp.getBanner(), isInterstitialImp, msgPrefix); + validateVideoMimes(imp.getVideo(), msgPrefix); + validateAudioMimes(imp.getAudio(), msgPrefix); + validatePmp(imp.getPmp(), msgPrefix); + } + + private void fillAndValidateNative(Native xNative, int impIndex) throws ValidationException { + if (xNative == null) { + return; + } + + final Request nativeRequest = parseNativeRequest(xNative.getRequest(), impIndex); + + validateNativeContextTypes(nativeRequest.getContext(), nativeRequest.getContextsubtype(), impIndex); + validateNativePlacementType(nativeRequest.getPlcmttype(), impIndex); + final List updatedAssets = validateAndGetUpdatedNativeAssets(nativeRequest.getAssets(), impIndex); + validateNativeEventTrackers(nativeRequest.getEventtrackers(), impIndex); + + // modifier was added to reduce memory consumption on updating bidRequest.imp[i].native.request object + xNative.setRequest(toEncodedRequest(nativeRequest, updatedAssets)); + } + + private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { + if (StringUtils.isBlank(rawStringNativeRequest)) { + throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); + } + try { + return mapper.mapper().readValue(rawStringNativeRequest, Request.class); + } catch (IOException e) { + throw new ValidationException("Error while parsing request.imp[%d].native.request: %s", + impIndex, + ExceptionUtils.getMessage(e)); + } + } + + private void validateNativeContextTypes(Integer context, Integer contextSubType, int index) + throws ValidationException { + + final int type = context != null ? context : 0; + if (type == 0) { + return; + } + + if (type < ContextType.CONTENT.getValue() + || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index); + } + + final int subType = contextSubType != null ? contextSubType : 0; + if (subType < 0) { + throw new ValidationException( + "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); + } + + if (subType == 0) { + return; + } + + if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) { + if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) { + if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) { + if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); + } + } + + private void validateNativePlacementType(Integer placementType, int index) throws ValidationException { + final int type = placementType != null ? placementType : 0; + if (type == 0) { + return; + } + + if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue() + && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type); + } + } + + private List validateAndGetUpdatedNativeAssets(List assets, int impIndex) throws ValidationException { + if (CollectionUtils.isEmpty(assets)) { + throw new ValidationException( + "request.imp[%d].native.request.assets must be an array containing at least one object", impIndex); + } + + final List updatedAssets = new ArrayList<>(); + for (int i = 0; i < assets.size(); i++) { + final Asset asset = assets.get(i); + validateNativeAsset(asset, impIndex, i); + + final Asset updatedAsset = asset.getId() != null ? asset : asset.toBuilder().id(i).build(); + final boolean hasAssetWithId = updatedAssets.stream() + .map(Asset::getId) + .anyMatch(id -> id.equals(updatedAsset.getId())); + + if (hasAssetWithId) { + throw new ValidationException("request.imp[%d].native.request.assets[%d].id is already being used by " + + "another asset. Each asset ID must be unique.", impIndex, i); + } + + updatedAssets.add(updatedAsset); + } + return updatedAssets; + } + + private void validateNativeAsset(Asset asset, int impIndex, int assetIndex) throws ValidationException { + final TitleObject title = asset.getTitle(); + final ImageObject image = asset.getImg(); + final VideoObject video = asset.getVideo(); + final DataObject data = asset.getData(); + + final long assetsCount = Stream.of(title, image, video, data) + .filter(Objects::nonNull) + .count(); + + if (assetsCount > 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", + impIndex, assetIndex); + } + + validateNativeAssetTitle(title, impIndex, assetIndex); + validateNativeAssetVideo(video, impIndex, assetIndex); + validateNativeAssetData(data, impIndex, assetIndex); + } + + private void validateNativeAssetTitle(TitleObject title, int impIndex, int assetIndex) throws ValidationException { + if (title != null && (title.getLen() == null || title.getLen() < 1)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].title.len must be a positive integer", + impIndex, assetIndex); + } + } + + private void validateNativeAssetData(DataObject data, int impIndex, int assetIndex) throws ValidationException { + if (data == null || data.getType() == null) { + return; + } + + final Integer type = data.getType(); + if (type < DataAssetType.SPONSORED.getValue() + || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: " + + documentationOnPage(40), impIndex, assetIndex); + } + } + + private void validateNativeAssetVideo(VideoObject video, int impIndex, int assetIndex) throws ValidationException { + if (video == null) { + return; + } + + if (CollectionUtils.isEmpty(video.getMimes())) { + throw new ValidationException("request.imp[%d].native.request.assets[%d].video.mimes must be an " + + "array with at least one MIME type", impIndex, assetIndex); + } + + if (video.getMinduration() == null || video.getMinduration() < 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", + impIndex, assetIndex); + } + + if (video.getMaxduration() == null || video.getMaxduration() < 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", + impIndex, assetIndex); + } + + validateNativeVideoProtocols(video.getProtocols(), impIndex, assetIndex); + } + + private void validateNativeVideoProtocols(List protocols, int impIndex, int assetIndex) + throws ValidationException { + if (CollectionUtils.isEmpty(protocols)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least" + + " one element", impIndex, assetIndex); + } + + for (int i = 0; i < protocols.size(); i++) { + validateNativeVideoProtocol(protocols.get(i), impIndex, assetIndex, i); + } + } + + private void validateNativeVideoProtocol(Integer protocol, int impIndex, int assetIndex, int protocolIndex) + throws ValidationException { + if (protocol < Protocol.VAST10.getValue() || protocol > Protocol.DAAST10_WRAPPER.getValue()) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]." + + " Got %d", impIndex, assetIndex, protocolIndex, protocol); + } + } + + private void validateNativeEventTrackers(List eventTrackers, int impIndex) + throws ValidationException { + + if (CollectionUtils.isNotEmpty(eventTrackers)) { + for (int eventTrackerIndex = 0; eventTrackerIndex < eventTrackers.size(); eventTrackerIndex++) { + validateNativeEventTracker(eventTrackers.get(eventTrackerIndex), impIndex, eventTrackerIndex); + } + } + } + + private void validateNativeEventTracker(EventTracker eventTracker, int impIndex, int eventIndex) + throws ValidationException { + if (eventTracker != null) { + final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0; + + if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue() + && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: " + + documentationOnPage(43), impIndex, eventIndex + ); + } + + final List methods = eventTracker.getMethods(); + + if (CollectionUtils.isEmpty(methods)) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: " + + documentationOnPage(43), impIndex, eventIndex + ); + } + + for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) { + final int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0; + if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue() + && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: " + + documentationOnPage(43), impIndex, eventIndex, methodIndex + ); + } + } + } + } + + private void validateImpExt(ObjectNode ext, Map aliases, int impIndex, + List warnings) throws ValidationException { + validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex, warnings); + } + + private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex, + List warnings) + throws ValidationException { + + if (extPrebidNode == null) { + throw new ValidationException( + "request.imp[%d].ext.prebid must be defined", impIndex); + } + + if (!extPrebidNode.isObject()) { + throw new ValidationException( + "request.imp[%d].ext.prebid must an object type", impIndex); + } + + final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT); + + if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder must be an object type", impIndex); + } + final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex); + + validateImpExtPrebidBidder(extPrebidBidderNode, extPrebid.getStoredAuctionResponse(), + aliases, impIndex, warnings); + validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex, warnings); + + validateImpExtPrebidImp(extPrebidNode.get(IMP_EXT), aliases, impIndex, warnings); + } + + private void validateImpExtPrebidImp(JsonNode imp, + Map aliases, + int impIndex, + List warnings) { + if (imp == null) { + return; + } + + final Iterator> bidders = imp.fields(); + while (bidders.hasNext()) { + final Map.Entry bidder = bidders.next(); + final String bidderName = bidder.getKey(); + final String resolvedBidderName = aliases.getOrDefault(bidderName, bidderName); + if (!bidderCatalog.isValidName(resolvedBidderName) && !bidderCatalog.isDeprecatedName(resolvedBidderName)) { + bidders.remove(); + warnings.add("WARNING: request.imp[%d].ext.prebid.imp.%s was dropped with the reason: invalid bidder" + .formatted(impIndex, bidderName)); + } + } + } + + private void validateImpExtPrebidBidder(JsonNode extPrebidBidder, + ExtStoredAuctionResponse storedAuctionResponse, + Map aliases, + int impIndex, + List warnings) throws ValidationException { + if (extPrebidBidder == null) { + if (storedAuctionResponse != null) { + return; + } else { + throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex); + } + } + + final Iterator> bidderExtensions = extPrebidBidder.fields(); + while (bidderExtensions.hasNext()) { + final Map.Entry bidderExtension = bidderExtensions.next(); + final String bidder = bidderExtension.getKey(); + try { + validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); + } catch (ValidationException ex) { + bidderExtensions.remove(); + warnings.add("WARNING: request.imp[%d].ext.prebid.bidder.%s was dropped with a reason: %s" + .formatted(impIndex, bidder, ex.getMessage())); + } + } + + if (extPrebidBidder.isEmpty()) { + warnings.add("WARNING: request.imp[%d].ext must contain at least one valid bidder".formatted(impIndex)); + } + } + + private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, + Map aliases, + int impIndex, + List warnings) throws ValidationException { + final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse(); + if (extStoredAuctionResponse != null) { + if (extStoredAuctionResponse.getSeatBids() != null) { + warnings.add("WARNING: request.imp[%d].ext.prebid.storedauctionresponse.seatbidarr".formatted(impIndex) + + " is not supported at the imp level"); + } + + if (extStoredAuctionResponse.getId() == null && extStoredAuctionResponse.getSeatBid() == null) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedauctionresponse.{id or seatbidobj} should be defined", + impIndex); + } + } + + final List storedBidResponses = extPrebid.getStoredBidResponse(); + if (CollectionUtils.isNotEmpty(storedBidResponses)) { + final ObjectNode bidderNode = extPrebid.getBidder(); + if (bidderNode == null || bidderNode.isEmpty()) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse" + .formatted(impIndex)); + } + + for (ExtStoredBidResponse storedBidResponse : storedBidResponses) { + validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex); + } + } + } + + private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode, + Map aliases, int impIndex) throws ValidationException { + final String bidder = extStoredBidResponse.getBidder(); + final String id = extStoredBidResponse.getId(); + if (StringUtils.isEmpty(bidder)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined".formatted(impIndex)); + } + + if (StringUtils.isEmpty(id)) { + throw new ValidationException( + "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id".formatted(impIndex)); + } + + final String resolvedBidder = aliases.getOrDefault(bidder, bidder); + + if (!bidderCatalog.isValidName(resolvedBidder)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder".formatted(impIndex)); + } + + final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames()) + .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder)); + if (noCorrespondentBidderParameters) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters" + .formatted(impIndex)); + } + } + + private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException { + try { + return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new ValidationException(" bidRequest.imp[%d].ext.prebid: %s has invalid format" + .formatted(impIndex, e.getMessage())); + } + } + + private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName) + throws ValidationException { + if (bidderCatalog.isValidName(bidderName)) { + final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); + if (!messages.isEmpty()) { + throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, + bidderName, String.join("\n", messages)); + } + } else if (!bidderCatalog.isDeprecatedName(bidderName)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); + } + } + + private void validatePmp(Pmp pmp, String msgPrefix) throws ValidationException { + if (pmp != null && pmp.getDeals() != null) { + for (int dealIndex = 0; dealIndex < pmp.getDeals().size(); dealIndex++) { + if (StringUtils.isBlank(pmp.getDeals().get(dealIndex).getId())) { + throw new ValidationException("%s.pmp.deals[%d] missing required field: \"id\"", + msgPrefix, dealIndex); + } + } + } + } + + private void validateBanner(Banner banner, boolean isInterstitial, String msgPrefix) throws ValidationException { + if (banner != null) { + final Integer width = banner.getW(); + final Integer height = banner.getH(); + final boolean hasWidth = hasPositiveValue(width); + final boolean hasHeight = hasPositiveValue(height); + final boolean hasSize = hasWidth && hasHeight; + + final List format = banner.getFormat(); + if (CollectionUtils.isEmpty(format) && !hasSize && !isInterstitial) { + throw new ValidationException("%s.banner has no sizes. Define \"w\" and \"h\", " + + "or include \"format\" elements", msgPrefix); + } + + if (width != null && height != null && !hasSize && !isInterstitial) { + throw new ValidationException("%s.banner must define a valid" + + " \"h\" and \"w\" properties", msgPrefix); + } + + if (format != null) { + for (int formatIndex = 0; formatIndex < format.size(); formatIndex++) { + validateFormat(format.get(formatIndex), msgPrefix, formatIndex); + } + } + } + } + + private void validateFormat(Format format, String msgPrefix, int formatIndex) throws ValidationException { + final boolean usesH = hasPositiveValue(format.getH()); + final boolean usesW = hasPositiveValue(format.getW()); + final boolean usesWmin = hasPositiveValue(format.getWmin()); + final boolean usesWratio = hasPositiveValue(format.getWratio()); + final boolean usesHratio = hasPositiveValue(format.getHratio()); + final boolean usesHW = usesH || usesW; + final boolean usesRatios = usesWmin || usesWratio || usesHratio; + + if (usesHW && usesRatios) { + throw new ValidationException("%s.banner.format[%d] should define *either*" + + " {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" " + + "objects in the request", msgPrefix, formatIndex); + } + + if (!usesHW && !usesRatios) { + throw new ValidationException("%s.banner.format[%d] should define *either*" + + " {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) " + + "to be non-zero positive", msgPrefix, formatIndex); + } + + if (usesHW && (!usesH || !usesW)) { + throw new ValidationException("%s.banner.format[%d] must define a valid" + + " \"h\" and \"w\" properties", msgPrefix, formatIndex); + } + + if (usesRatios && (!usesWmin || !usesWratio || !usesHratio)) { + throw new ValidationException("%s.banner.format[%d] must define a valid" + + " \"wmin\", \"wratio\", and \"hratio\" properties", msgPrefix, formatIndex); + } + } + + private void validateVideoMimes(Video video, String msgPrefix) throws ValidationException { + if (video != null) { + validateMimes(video.getMimes(), + "%s.video.mimes must contain at least one supported MIME type", msgPrefix); + } + } + + private void validateAudioMimes(Audio audio, String msgPrefix) throws ValidationException { + if (audio != null) { + validateMimes(audio.getMimes(), + "%s.audio.mimes must contain at least one supported MIME type", msgPrefix); + } + } + + private void validateMimes(List mimes, String msg, String msgPrefix) throws ValidationException { + if (CollectionUtils.isEmpty(mimes)) { + throw new ValidationException(msg, msgPrefix); + } + } + + private void validateMetrics(List metrics, String msgPrefix) throws ValidationException { + for (int i = 0; i < metrics.size(); i++) { + final Metric metric = metrics.get(i); + + if (StringUtils.isEmpty(metric.getType())) { + throw new ValidationException("Missing %s.metric[%d].type", msgPrefix, i); + } + + final Float value = metric.getValue(); + if (value == null || value < 0.0 || value > 1.0) { + throw new ValidationException("%s.metric[%d].value must be in the range [0.0, 1.0]", msgPrefix, i); + } + } + } + + private String toEncodedRequest(Request nativeRequest, List updatedAssets) { + try { + return mapper.mapper().writeValueAsString(nativeRequest.toBuilder().assets(updatedAssets).build()); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Error while marshaling native request to the string", e); + } + } + + private static String documentationOnPage(int page) { + return "%s#page=%d".formatted(DOCUMENTATION, page); + } + + private static boolean hasPositiveValue(Integer value) { + return value != null && value > 0; + } + +} diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index 147ffd60208..f596850955d 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -3,45 +3,22 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Asset; -import com.iab.openrtb.request.Audio; -import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.DataObject; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.EventTracker; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.ImageObject; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Metric; -import com.iab.openrtb.request.Native; -import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Request; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.TitleObject; -import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; -import com.iab.openrtb.request.VideoObject; -import com.iab.openrtb.request.ntv.ContextSubType; -import com.iab.openrtb.request.ntv.ContextType; -import com.iab.openrtb.request.ntv.DataAssetType; -import com.iab.openrtb.request.ntv.EventTrackingMethod; -import com.iab.openrtb.request.ntv.EventType; -import com.iab.openrtb.request.ntv.PlacementType; -import com.iab.openrtb.request.ntv.Protocol; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.HttpRequestContext; @@ -49,7 +26,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -60,30 +36,24 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.StreamUtil; import org.prebid.server.validation.model.ValidationResult; -import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; /** * A component that validates {@link BidRequest} objects for openrtb2 auction endpoint. @@ -94,17 +64,11 @@ public class RequestValidator { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( LoggerFactory.getLogger(RequestValidator.class)); - private static final String PREBID_EXT = "prebid"; - private static final String BIDDER_EXT = "bidder"; private static final String ASTERISK = "*"; private static final Locale LOCALE = Locale.US; - private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500; - - private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; private final BidderCatalog bidderCatalog; - private final BidderParamValidator bidderParamValidator; + private final ImpValidator impValidator; private final Metrics metrics; private final JacksonMapper mapper; private final double logSamplingRate; @@ -115,14 +79,13 @@ public class RequestValidator { * properties of bidRequest. */ public RequestValidator(BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, - Metrics metrics, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, double logSamplingRate, boolean enabledStrictAppSiteDoohValidation) { this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); + this.impValidator = Objects.requireNonNull(impValidator); this.metrics = Objects.requireNonNull(metrics); this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; @@ -186,9 +149,7 @@ public ValidationResult validate(BidRequest bidRequest, HttpRequestContext httpR throw new ValidationException(String.join(System.lineSeparator(), errors)); } - for (int index = 0; index < bidRequest.getImp().size(); index++) { - validateImp(bidRequest.getImp().get(index), aliases, index, warnings); - } + impValidator.validateImps(bidRequest.getImp(), aliases, warnings); final List channels = new ArrayList<>(); Optional.ofNullable(bidRequest.getApp()).ifPresent(ignored -> channels.add("request.app")); @@ -610,19 +571,6 @@ private void validateUser(User user, Map aliases) throws Validat throw new ValidationException( "request.user.eids[%d] missing required field: \"source\"", index); } - final List eidUids = eid.getUids(); - if (CollectionUtils.isEmpty(eidUids)) { - throw new ValidationException( - "request.user.eids[%d].uids must contain at least one element", index); - } - for (int uidsIndex = 0; uidsIndex < eidUids.size(); uidsIndex++) { - final Uid uid = eidUids.get(uidsIndex); - if (StringUtils.isBlank(uid.getId())) { - throw new ValidationException( - "request.user.eids[%d].uids[%d] missing required field: \"id\"", index, - uidsIndex); - } - } } } } @@ -660,548 +608,4 @@ private void validateRegs(Regs regs) throws ValidationException { } } - private void validateImp(Imp imp, Map aliases, int index, List warnings) - throws ValidationException { - if (StringUtils.isBlank(imp.getId())) { - throw new ValidationException("request.imp[%d] missing required field: \"id\"", index); - } - if (imp.getMetric() != null && !imp.getMetric().isEmpty()) { - validateMetrics(imp.getMetric(), index); - } - if (imp.getBanner() == null && imp.getVideo() == null && imp.getAudio() == null && imp.getXNative() == null) { - throw new ValidationException( - "request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", - index); - } - - final boolean isInterstitialImp = Objects.equals(imp.getInstl(), 1); - validateBanner(imp.getBanner(), isInterstitialImp, index); - validateVideoMimes(imp.getVideo(), index); - validateAudioMimes(imp.getAudio(), index); - fillAndValidateNative(imp.getXNative(), index); - validatePmp(imp.getPmp(), index); - validateImpExt(imp.getExt(), aliases, index, warnings); - } - - private void fillAndValidateNative(Native xNative, int impIndex) throws ValidationException { - if (xNative == null) { - return; - } - - final Request nativeRequest = parseNativeRequest(xNative.getRequest(), impIndex); - - validateNativeContextTypes(nativeRequest.getContext(), nativeRequest.getContextsubtype(), impIndex); - validateNativePlacementType(nativeRequest.getPlcmttype(), impIndex); - final List updatedAssets = validateAndGetUpdatedNativeAssets(nativeRequest.getAssets(), impIndex); - validateNativeEventTrackers(nativeRequest.getEventtrackers(), impIndex); - - // modifier was added to reduce memory consumption on updating bidRequest.imp[i].native.request object - xNative.setRequest(toEncodedRequest(nativeRequest, updatedAssets)); - } - - private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { - if (StringUtils.isBlank(rawStringNativeRequest)) { - throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); - } - try { - return mapper.mapper().readValue(rawStringNativeRequest, Request.class); - } catch (IOException e) { - throw new ValidationException("Error while parsing request.imp[%d].native.request: %s", - impIndex, - ExceptionUtils.getMessage(e)); - } - } - - private void validateNativeContextTypes(Integer context, Integer contextSubType, int index) - throws ValidationException { - - final int type = context != null ? context : 0; - if (type == 0) { - return; - } - - if (type < ContextType.CONTENT.getValue() - || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index); - } - - final int subType = contextSubType != null ? contextSubType : 0; - if (subType < 0) { - throw new ValidationException( - "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); - } - - if (subType == 0) { - return; - } - - if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) { - if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) { - if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) { - if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); - } - } - - private void validateNativePlacementType(Integer placementType, int index) throws ValidationException { - final int type = placementType != null ? placementType : 0; - if (type == 0) { - return; - } - - if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue() - && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type); - } - } - - private List validateAndGetUpdatedNativeAssets(List assets, int impIndex) throws ValidationException { - if (CollectionUtils.isEmpty(assets)) { - throw new ValidationException( - "request.imp[%d].native.request.assets must be an array containing at least one object", impIndex); - } - - final List updatedAssets = new ArrayList<>(); - for (int i = 0; i < assets.size(); i++) { - final Asset asset = assets.get(i); - validateNativeAsset(asset, impIndex, i); - - final Asset updatedAsset = asset.getId() != null ? asset : asset.toBuilder().id(i).build(); - final boolean hasAssetWithId = updatedAssets.stream() - .map(Asset::getId) - .anyMatch(id -> id.equals(updatedAsset.getId())); - - if (hasAssetWithId) { - throw new ValidationException("request.imp[%d].native.request.assets[%d].id is already being used by " - + "another asset. Each asset ID must be unique.", impIndex, i); - } - - updatedAssets.add(updatedAsset); - } - return updatedAssets; - } - - private void validateNativeAsset(Asset asset, int impIndex, int assetIndex) throws ValidationException { - final TitleObject title = asset.getTitle(); - final ImageObject image = asset.getImg(); - final VideoObject video = asset.getVideo(); - final DataObject data = asset.getData(); - - final long assetsCount = Stream.of(title, image, video, data) - .filter(Objects::nonNull) - .count(); - - if (assetsCount > 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", - impIndex, assetIndex); - } - - validateNativeAssetTitle(title, impIndex, assetIndex); - validateNativeAssetVideo(video, impIndex, assetIndex); - validateNativeAssetData(data, impIndex, assetIndex); - } - - private void validateNativeAssetTitle(TitleObject title, int impIndex, int assetIndex) throws ValidationException { - if (title != null && (title.getLen() == null || title.getLen() < 1)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].title.len must be a positive integer", - impIndex, assetIndex); - } - } - - private void validateNativeAssetData(DataObject data, int impIndex, int assetIndex) throws ValidationException { - if (data == null || data.getType() == null) { - return; - } - - final Integer type = data.getType(); - if (type < DataAssetType.SPONSORED.getValue() - || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: " - + documentationOnPage(40), impIndex, assetIndex); - } - } - - private void validateNativeAssetVideo(VideoObject video, int impIndex, int assetIndex) throws ValidationException { - if (video == null) { - return; - } - - if (CollectionUtils.isEmpty(video.getMimes())) { - throw new ValidationException("request.imp[%d].native.request.assets[%d].video.mimes must be an " - + "array with at least one MIME type", impIndex, assetIndex); - } - - if (video.getMinduration() == null || video.getMinduration() < 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", - impIndex, assetIndex); - } - - if (video.getMaxduration() == null || video.getMaxduration() < 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", - impIndex, assetIndex); - } - - validateNativeVideoProtocols(video.getProtocols(), impIndex, assetIndex); - } - - private void validateNativeVideoProtocols(List protocols, int impIndex, int assetIndex) - throws ValidationException { - if (CollectionUtils.isEmpty(protocols)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least" - + " one element", impIndex, assetIndex); - } - - for (int i = 0; i < protocols.size(); i++) { - validateNativeVideoProtocol(protocols.get(i), impIndex, assetIndex, i); - } - } - - private void validateNativeVideoProtocol(Integer protocol, int impIndex, int assetIndex, int protocolIndex) - throws ValidationException { - if (protocol < Protocol.VAST10.getValue() || protocol > Protocol.DAAST10_WRAPPER.getValue()) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]." - + " Got %d", impIndex, assetIndex, protocolIndex, protocol); - } - } - - private void validateNativeEventTrackers(List eventTrackers, int impIndex) - throws ValidationException { - - if (CollectionUtils.isNotEmpty(eventTrackers)) { - for (int eventTrackerIndex = 0; eventTrackerIndex < eventTrackers.size(); eventTrackerIndex++) { - validateNativeEventTracker(eventTrackers.get(eventTrackerIndex), impIndex, eventTrackerIndex); - } - } - } - - private void validateNativeEventTracker(EventTracker eventTracker, int impIndex, int eventIndex) - throws ValidationException { - if (eventTracker != null) { - final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0; - - if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue() - && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: " - + documentationOnPage(43), impIndex, eventIndex - ); - } - - final List methods = eventTracker.getMethods(); - - if (CollectionUtils.isEmpty(methods)) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: " - + documentationOnPage(43), impIndex, eventIndex - ); - } - - for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) { - final int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0; - if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue() - && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: " - + documentationOnPage(43), impIndex, eventIndex, methodIndex - ); - } - } - } - } - - private static String documentationOnPage(int page) { - return "%s#page=%d".formatted(DOCUMENTATION, page); - } - - private String toEncodedRequest(Request nativeRequest, List updatedAssets) { - try { - return mapper.mapper().writeValueAsString(nativeRequest.toBuilder().assets(updatedAssets).build()); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Error while marshaling native request to the string", e); - } - } - - private void validateImpExt(ObjectNode ext, Map aliases, int impIndex, - List warnings) throws ValidationException { - validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex, warnings); - } - - private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex, - List warnings) - throws ValidationException { - - if (extPrebidNode == null) { - throw new ValidationException( - "request.imp[%d].ext.prebid must be defined", impIndex); - } - - if (!extPrebidNode.isObject()) { - throw new ValidationException( - "request.imp[%d].ext.prebid must an object type", impIndex); - } - - final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT); - - if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder must be an object type", impIndex); - } - final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex); - - validateImpExtPrebidBidder(extPrebidBidderNode, extPrebid.getStoredAuctionResponse(), - aliases, impIndex, warnings); - validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex); - } - - private void validateImpExtPrebidBidder(JsonNode extPrebidBidder, - ExtStoredAuctionResponse storedAuctionResponse, - Map aliases, - int impIndex, - List warnings) throws ValidationException { - if (extPrebidBidder == null) { - if (storedAuctionResponse != null) { - return; - } else { - throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex); - } - } - - final Iterator> bidderExtensions = extPrebidBidder.fields(); - while (bidderExtensions.hasNext()) { - final Map.Entry bidderExtension = bidderExtensions.next(); - final String bidder = bidderExtension.getKey(); - try { - validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); - } catch (ValidationException ex) { - bidderExtensions.remove(); - warnings.add("WARNING: request.imp[%d].ext.prebid.bidder.%s was dropped with a reason: %s" - .formatted(impIndex, bidder, ex.getMessage())); - } - } - - if (extPrebidBidder.size() == 0) { - warnings.add("WARNING: request.imp[%d].ext must contain at least one valid bidder".formatted(impIndex)); - } - } - - private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, - Map aliases, - int impIndex) throws ValidationException { - final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse(); - if (extStoredAuctionResponse != null && extStoredAuctionResponse.getId() == null) { - throw new ValidationException("request.imp[%d].ext.prebid.storedauctionresponse.id should be defined", - impIndex); - } - - final List storedBidResponses = extPrebid.getStoredBidResponse(); - if (CollectionUtils.isNotEmpty(storedBidResponses)) { - final ObjectNode bidderNode = extPrebid.getBidder(); - if (bidderNode == null || bidderNode.isEmpty()) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse" - .formatted(impIndex)); - } - - for (ExtStoredBidResponse storedBidResponse : storedBidResponses) { - validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex); - } - } - } - - private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode, - Map aliases, int impIndex) throws ValidationException { - final String bidder = extStoredBidResponse.getBidder(); - final String id = extStoredBidResponse.getId(); - if (StringUtils.isEmpty(bidder)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined".formatted(impIndex)); - } - - if (StringUtils.isEmpty(id)) { - throw new ValidationException( - "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id".formatted(impIndex)); - } - - final String resolvedBidder = aliases.getOrDefault(bidder, bidder); - - if (!bidderCatalog.isValidName(resolvedBidder)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder".formatted(impIndex)); - } - - final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames()) - .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder)); - if (noCorrespondentBidderParameters) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters" - .formatted(impIndex)); - } - } - - private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new ValidationException(" bidRequest.imp[%d].ext.prebid: %s has invalid format" - .formatted(impIndex, e.getMessage())); - } - } - - private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName) - throws ValidationException { - if (bidderCatalog.isValidName(bidderName)) { - final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); - if (!messages.isEmpty()) { - throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, - bidderName, String.join("\n", messages)); - } - } else if (!bidderCatalog.isDeprecatedName(bidderName)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); - } - } - - private void validatePmp(Pmp pmp, int impIndex) throws ValidationException { - if (pmp != null && pmp.getDeals() != null) { - for (int dealIndex = 0; dealIndex < pmp.getDeals().size(); dealIndex++) { - if (StringUtils.isBlank(pmp.getDeals().get(dealIndex).getId())) { - throw new ValidationException("request.imp[%d].pmp.deals[%d] missing required field: \"id\"", - impIndex, dealIndex); - } - } - } - } - - private void validateBanner(Banner banner, boolean isInterstitial, int impIndex) throws ValidationException { - if (banner != null) { - final Integer width = banner.getW(); - final Integer height = banner.getH(); - final boolean hasWidth = hasPositiveValue(width); - final boolean hasHeight = hasPositiveValue(height); - final boolean hasSize = hasWidth && hasHeight; - - final List format = banner.getFormat(); - if (CollectionUtils.isEmpty(format) && !hasSize && !isInterstitial) { - throw new ValidationException("request.imp[%d].banner has no sizes. Define \"w\" and \"h\", " - + "or include \"format\" elements", impIndex); - } - - if (width != null && height != null && !hasSize && !isInterstitial) { - throw new ValidationException("Request imp[%d].banner must define a valid" - + " \"h\" and \"w\" properties", impIndex); - } - - if (format != null) { - for (int formatIndex = 0; formatIndex < format.size(); formatIndex++) { - validateFormat(format.get(formatIndex), impIndex, formatIndex); - } - } - } - } - - private void validateFormat(Format format, int impIndex, int formatIndex) throws ValidationException { - final boolean usesH = hasPositiveValue(format.getH()); - final boolean usesW = hasPositiveValue(format.getW()); - final boolean usesWmin = hasPositiveValue(format.getWmin()); - final boolean usesWratio = hasPositiveValue(format.getWratio()); - final boolean usesHratio = hasPositiveValue(format.getHratio()); - final boolean usesHW = usesH || usesW; - final boolean usesRatios = usesWmin || usesWratio || usesHratio; - - if (usesHW && usesRatios) { - throw new ValidationException("Request imp[%d].banner.format[%d] should define *either*" - + " {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" " - + "objects in the request", impIndex, formatIndex); - } - - if (!usesHW && !usesRatios) { - throw new ValidationException("Request imp[%d].banner.format[%d] should define *either*" - + " {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) " - + "to be non-zero positive", impIndex, formatIndex); - } - - if (usesHW && (!usesH || !usesW)) { - throw new ValidationException("Request imp[%d].banner.format[%d] must define a valid" - + " \"h\" and \"w\" properties", impIndex, formatIndex); - } - - if (usesRatios && (!usesWmin || !usesWratio || !usesHratio)) { - throw new ValidationException("Request imp[%d].banner.format[%d] must define a valid" - + " \"wmin\", \"wratio\", and \"hratio\" properties", impIndex, formatIndex); - } - } - - private void validateVideoMimes(Video video, int impIndex) throws ValidationException { - if (video != null) { - validateMimes(video.getMimes(), - "request.imp[%d].video.mimes must contain at least one supported MIME type", impIndex); - } - } - - private void validateAudioMimes(Audio audio, int impIndex) throws ValidationException { - if (audio != null) { - validateMimes(audio.getMimes(), - "request.imp[%d].audio.mimes must contain at least one supported MIME type", impIndex); - } - } - - private void validateMimes(List mimes, String msg, int index) throws ValidationException { - if (CollectionUtils.isEmpty(mimes)) { - throw new ValidationException(msg, index); - } - } - - private void validateMetrics(List metrics, int impIndex) throws ValidationException { - for (int i = 0; i < metrics.size(); i++) { - final Metric metric = metrics.get(i); - - if (StringUtils.isEmpty(metric.getType())) { - throw new ValidationException("Missing request.imp[%d].metric[%d].type", impIndex, i); - } - - final Float value = metric.getValue(); - if (value == null || value < 0.0 || value > 1.0) { - throw new ValidationException("request.imp[%d].metric[%d].value must be in the range [0.0, 1.0]", - impIndex, i); - } - } - } - - private static boolean hasPositiveValue(Integer value) { - return value != null && value > 0; - } } diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java index 353f4389a14..10b939ae5dd 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -1,30 +1,24 @@ package org.prebid.server.validation; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Deal; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Site; import com.iab.openrtb.response.Bid; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; -import org.prebid.server.proto.openrtb.ext.request.ExtDeal; -import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; @@ -33,15 +27,11 @@ import org.prebid.server.validation.model.ValidationResult; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Currency; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Validator for response {@link Bid} object. @@ -58,31 +48,21 @@ public class ResponseBidValidator { private static final String[] INSECURE_MARKUP_MARKERS = {"http:", "http%3A"}; private static final String[] SECURE_MARKUP_MARKERS = {"https:", "https%3A"}; - private static final String PREBID_EXT = "prebid"; - private static final String BIDDER_EXT = "bidder"; - private static final String DEALS_ONLY = "dealsonly"; - private final BidValidationEnforcement bannerMaxSizeEnforcement; private final BidValidationEnforcement secureMarkupEnforcement; private final Metrics metrics; - private final JacksonMapper mapper; - private final boolean dealsEnabled; private final double logSamplingRate; public ResponseBidValidator(BidValidationEnforcement bannerMaxSizeEnforcement, BidValidationEnforcement secureMarkupEnforcement, Metrics metrics, - JacksonMapper mapper, - boolean dealsEnabled, double logSamplingRate) { this.bannerMaxSizeEnforcement = Objects.requireNonNull(bannerMaxSizeEnforcement); this.secureMarkupEnforcement = Objects.requireNonNull(secureMarkupEnforcement); this.metrics = Objects.requireNonNull(metrics); - this.mapper = Objects.requireNonNull(mapper); - this.dealsEnabled = dealsEnabled; this.logSamplingRate = logSamplingRate; } @@ -94,6 +74,7 @@ public ValidationResult validate(BidderBid bidderBid, final Bid bid = bidderBid.getBid(); final BidRequest bidRequest = auctionContext.getBidRequest(); final Account account = auctionContext.getAccount(); + final BidRejectionTracker bidRejectionTracker = auctionContext.getBidRejectionTrackers().get(bidder); final List warnings = new ArrayList<>(); try { @@ -103,14 +84,25 @@ public ValidationResult validate(BidderBid bidderBid, final Imp correspondingImp = findCorrespondingImp(bid, bidRequest); if (bidderBid.getType() == BidType.banner) { - warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases)); + warnings.addAll(validateBannerFields( + bid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); } - if (dealsEnabled) { - validateDealsFor(bidderBid, bidRequest, bidder, aliases, warnings); - } + warnings.addAll(validateSecureMarkup( + bid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); - warnings.addAll(validateSecureMarkup(bid, bidder, bidRequest, account, correspondingImp, aliases)); } catch (ValidationException e) { return ValidationResult.error(warnings, e.getMessage()); } @@ -174,7 +166,8 @@ private List validateBannerFields(Bid bid, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { final BidValidationEnforcement bannerMaxSizeEnforcement = effectiveBannerMaxSizeEnforcement(account); if (bannerMaxSizeEnforcement != BidValidationEnforcement.skip) { @@ -196,6 +189,10 @@ private List validateBannerFields(Bid bid, bid.getW(), bid.getH()); + bidRejectionTracker.reject( + correspondingImp.getId(), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); + return singleWarningOrValidationException( bannerMaxSizeEnforcement, metricName -> metrics.updateSizeValidationMetrics( @@ -244,7 +241,8 @@ private List validateSecureMarkup(Bid bid, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { if (secureMarkupEnforcement == BidValidationEnforcement.skip) { return Collections.emptyList(); @@ -260,6 +258,10 @@ private List validateSecureMarkup(Bid bid, creative validation for bid %s, account=%s, referrer=%s, adm=%s""" .formatted(secureMarkupEnforcement, bidder, bid.getId(), accountId, referer, adm); + bidRejectionTracker.reject( + correspondingImp.getId(), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); + return singleWarningOrValidationException( secureMarkupEnforcement, metricName -> metrics.updateSecureValidationMetrics( @@ -280,9 +282,9 @@ private static boolean markupIsNotSecure(String adm) { } private List singleWarningOrValidationException(BidValidationEnforcement enforcement, - Consumer metricsRecorder, - ConditionalLogger conditionalLogger, - String message) throws ValidationException { + Consumer metricsRecorder, + ConditionalLogger conditionalLogger, + String message) throws ValidationException { return switch (enforcement) { case enforce -> { metricsRecorder.accept(MetricName.err); @@ -302,165 +304,4 @@ private static String getReferer(BidRequest bidRequest) { final Site site = bidRequest.getSite(); return site != null ? site.getPage() : "unknown"; } - - private void validateDealsFor(BidderBid bidderBid, - BidRequest bidRequest, - String bidder, - BidderAliases aliases, - List warnings) throws ValidationException { - - final Bid bid = bidderBid.getBid(); - final String bidId = bid.getId(); - - final Imp imp = bidRequest.getImp().stream() - .filter(curImp -> Objects.equals(curImp.getId(), bid.getImpid())) - .findFirst() - .orElseThrow(() -> new ValidationException("Bid \"%s\" has no corresponding imp in request", bidId)); - - final String dealId = bid.getDealid(); - - if (isDealsOnlyImp(imp, bidder) && dealId == null) { - throw new ValidationException("Bid \"%s\" missing required field 'dealid'", bidId); - } - - if (dealId != null) { - final Set dealIdsFromImp = getDealIdsFromImp(imp, bidder, aliases); - if (CollectionUtils.isNotEmpty(dealIdsFromImp) && !dealIdsFromImp.contains(dealId)) { - warnings.add(""" - WARNING: Bid "%s" has 'dealid' not present in corresponding imp in request. \ - 'dealid' in bid: '%s', deal Ids in imp: '%s'""" - .formatted(bidId, dealId, String.join(",", dealIdsFromImp))); - } - if (bidderBid.getType() == BidType.banner) { - if (imp.getBanner() == null) { - throw new ValidationException(""" - Bid "%s" has banner media type but corresponding imp \ - in request is missing 'banner' object""", - bidId); - } - - final List bannerFormats = getBannerFormats(imp); - if (bidSizeNotInFormats(bid, bannerFormats)) { - throw new ValidationException(""" - Bid "%s" has 'w' and 'h' not supported by corresponding imp in \ - request. Bid dimensions: '%dx%d', formats in imp: '%s'""", - bidId, - bid.getW(), - bid.getH(), - formatSizes(bannerFormats)); - } - - if (isPgDeal(imp, dealId)) { - validateIsInLineItemSizes(bid, bidId, dealId, imp); - } - } - } - } - - private void validateIsInLineItemSizes(Bid bid, String bidId, String dealId, Imp imp) throws ValidationException { - final List lineItemSizes = getLineItemSizes(imp, dealId); - if (lineItemSizes.isEmpty()) { - throw new ValidationException( - "Line item sizes were not found for bidId %s and dealId %s", bid.getId(), dealId); - } - - if (bidSizeNotInFormats(bid, lineItemSizes)) { - throw new ValidationException( - """ - Bid "%s" has 'w' and 'h' not matched to Line Item. \ - Bid dimensions: '%dx%d', Line Item sizes: '%s'""", - bidId, bid.getW(), bid.getH(), formatSizes(lineItemSizes)); - } - } - - private static boolean isDealsOnlyImp(Imp imp, String bidder) { - final JsonNode dealsOnlyNode = bidderParamsFromImp(imp).path(bidder).path(DEALS_ONLY); - return dealsOnlyNode.isBoolean() && dealsOnlyNode.asBoolean(); - } - - private static JsonNode bidderParamsFromImp(Imp imp) { - return imp.getExt().path(PREBID_EXT).path(BIDDER_EXT); - } - - private Set getDealIdsFromImp(Imp imp, String bidder, BidderAliases aliases) { - return getDeals(imp) - .filter(Objects::nonNull) - .filter(deal -> isBidderHasDeal(bidder, dealExt(deal.getExt()), aliases)) - .map(Deal::getId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - private static Stream getDeals(Imp imp) { - final Pmp pmp = imp.getPmp(); - return pmp != null ? pmp.getDeals().stream() : Stream.empty(); - } - - private static boolean isBidderHasDeal(String bidder, ExtDeal extDeal, BidderAliases aliases) { - final ExtDealLine extDealLine = extDeal != null ? extDeal.getLine() : null; - final String dealLineBidder = extDealLine != null ? extDealLine.getBidder() : null; - return dealLineBidder == null || aliases.isSame(bidder, dealLineBidder); - } - - private static boolean bidSizeNotInFormats(Bid bid, List formats) { - return formats.stream() - .noneMatch(format -> sizesEqual(bid, format)); - } - - private static boolean sizesEqual(Bid bid, Format format) { - return Objects.equals(format.getH(), bid.getH()) && Objects.equals(format.getW(), bid.getW()); - } - - private static List getBannerFormats(Imp imp) { - return ListUtils.emptyIfNull(imp.getBanner().getFormat()); - } - - private List getLineItemSizes(Imp imp, String dealId) { - return getDeals(imp) - .filter(deal -> dealId.equals(deal.getId())) - .map(Deal::getExt) - .filter(Objects::nonNull) - .map(this::dealExt) - .filter(Objects::nonNull) - .map(ExtDeal::getLine) - .filter(Objects::nonNull) - .map(ExtDealLine::getSizes) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .toList(); - } - - private boolean isPgDeal(Imp imp, String dealId) { - return getDeals(imp) - .filter(Objects::nonNull) - .filter(deal -> Objects.equals(deal.getId(), dealId)) - .map(Deal::getExt) - .filter(Objects::nonNull) - .map(this::dealExt) - .filter(Objects::nonNull) - .map(ExtDeal::getLine) - .filter(Objects::nonNull) - .map(ExtDealLine::getLineItemId) - .anyMatch(Objects::nonNull); - } - - private ExtDeal dealExt(JsonNode ext) { - try { - return mapper.mapper().treeToValue(ext, ExtDeal.class); - } catch (JsonProcessingException e) { - logger.warn("Error decoding deal.ext: {0}", e, e.getMessage()); - return null; - } - } - - private static String formatSizes(List lineItemSizes) { - return lineItemSizes.stream() - .map(ResponseBidValidator::formatSize) - .collect(Collectors.joining(",")); - } - - private static String formatSize(Format lineItemSize) { - return "%dx%d".formatted(lineItemSize.getW(), lineItemSize.getH()); - } } diff --git a/src/main/java/org/prebid/server/validation/ValidationException.java b/src/main/java/org/prebid/server/validation/ValidationException.java index 792d8a047bc..c5f4efae562 100644 --- a/src/main/java/org/prebid/server/validation/ValidationException.java +++ b/src/main/java/org/prebid/server/validation/ValidationException.java @@ -1,12 +1,12 @@ package org.prebid.server.validation; -class ValidationException extends Exception { +public class ValidationException extends Exception { - ValidationException(String errorMessageFormat) { + public ValidationException(String errorMessageFormat) { super(errorMessageFormat); } - ValidationException(String errorMessageFormat, Object... args) { + public ValidationException(String errorMessageFormat, Object... args) { super(errorMessageFormat.formatted(args)); } } diff --git a/src/main/java/org/prebid/server/validation/VideoRequestValidator.java b/src/main/java/org/prebid/server/validation/VideoRequestValidator.java index 354db143e0a..be8c553d735 100644 --- a/src/main/java/org/prebid/server/validation/VideoRequestValidator.java +++ b/src/main/java/org/prebid/server/validation/VideoRequestValidator.java @@ -25,8 +25,10 @@ public class VideoRequestValidator { /** * Throws {@link InvalidRequestException} in case of invalid {@link BidRequestVideo}. */ - public void validateStoredBidRequest(BidRequestVideo bidRequestVideo, boolean enforceStoredRequest, - List blacklistedAccounts) { + public void validateStoredBidRequest(BidRequestVideo bidRequestVideo, + boolean enforceStoredRequest, + List blocklistedAccounts) { + if (enforceStoredRequest && StringUtils.isBlank(bidRequestVideo.getStoredrequestid())) { throw new InvalidRequestException("request missing required field: storedrequestid"); } @@ -44,7 +46,7 @@ public void validateStoredBidRequest(BidRequestVideo bidRequestVideo, boolean en } } - validateSiteAndApp(bidRequestVideo.getSite(), bidRequestVideo.getApp(), blacklistedAccounts); + validateSiteAndApp(bidRequestVideo.getSite(), bidRequestVideo.getApp(), blocklistedAccounts); validateVideo(bidRequestVideo.getVideo()); } @@ -53,7 +55,7 @@ private static boolean isZeroOrNegativeDuration(List durationRangeSec) .anyMatch(duration -> duration <= 0); } - private void validateSiteAndApp(Site site, App app, List blacklistedAccounts) { + private void validateSiteAndApp(Site site, App app, List blocklistedAccounts) { if (app == null && site == null) { throw new InvalidRequestException("request missing required field: site or app"); } else if (app != null && site != null) { @@ -64,7 +66,7 @@ private void validateSiteAndApp(Site site, App app, List blacklistedAcco final String appId = app.getId(); if (StringUtils.isNotBlank(appId)) { - if (blacklistedAccounts.contains(appId)) { + if (blocklistedAccounts.contains(appId)) { throw new InvalidRequestException("Prebid-server does not process requests from App ID: " + appId); } diff --git a/src/main/java/org/prebid/server/vast/VastModifier.java b/src/main/java/org/prebid/server/vast/VastModifier.java index ce80078bfc1..f549c407ab7 100644 --- a/src/main/java/org/prebid/server/vast/VastModifier.java +++ b/src/main/java/org/prebid/server/vast/VastModifier.java @@ -5,7 +5,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.cache.proto.request.PutObject; +import org.prebid.server.cache.proto.request.bid.BidPutObject; import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.PreBidException; @@ -14,15 +14,24 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class VastModifier { - private static final String IN_LINE_TAG = ""; - private static final String IN_LINE_CLOSE_TAG = ""; - private static final String WRAPPER_TAG = ""; - private static final String WRAPPER_CLOSE_TAG = ""; - private static final String IMPRESSION_CLOSE_TAG = ""; + private static final Pattern WRAPPER_OPEN_TAG_PATTERN = + Pattern.compile("<\\s*wrapper(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern WRAPPER_CLOSE_TAG_PATTERN = + Pattern.compile("<\\s*/\\s*wrapper(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern INLINE_OPEN_TAG_PATTERN = + Pattern.compile("<\\s*inline(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern INLINE_CLOSE_TAG_PATTERN = + Pattern.compile("<\\s*/\\s*inline(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern IMPRESSION_CLOSE_TAG_PATTERN = + Pattern.compile("<\\s*/\\s*impression(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private final BidderCatalog bidderCatalog; private final EventsService eventsService; private final Metrics metrics; @@ -35,23 +44,22 @@ public VastModifier(BidderCatalog bidderCatalog, EventsService eventsService, Me public JsonNode modifyVastXml(Boolean isEventsEnabled, Set allowedBidders, - PutObject putObject, + BidPutObject bidPutObject, String accountId, String integration) { - final JsonNode value = putObject.getValue(); - final String bidder = putObject.getBidder(); + final JsonNode value = bidPutObject.getValue(); + final String bidder = bidPutObject.getBidder(); final boolean isValueValid = value != null && !value.isNull(); if (BooleanUtils.isTrue(isEventsEnabled) && allowedBidders.contains(bidder) && isValueValid) { final EventsContext eventsContext = EventsContext.builder() - .auctionId(putObject.getAid()) - .auctionTimestamp(putObject.getTimestamp()) + .auctionId(bidPutObject.getAid()) + .auctionTimestamp(bidPutObject.getTimestamp()) .integration(integration) .build(); final String vastUrlTracking = eventsService.vastUrlTracking( - putObject.getBidid(), + bidPutObject.getBidid(), bidder, accountId, - null, eventsContext); try { return new TextNode(appendTrackingUrlToVastXml(value.asText(), vastUrlTracking, bidder)); @@ -69,8 +77,8 @@ public String createBidVastXml(String bidder, String eventBidId, String accountId, EventsContext eventsContext, - List debugWarnings, - String lineItemId) { + List debugWarnings) { + if (!bidderCatalog.isModifyingVastXmlAllowed(bidder)) { return bidAdm; } @@ -80,8 +88,7 @@ public String createBidVastXml(String bidder, return vastXml; } - final String vastUrl = eventsService.vastUrlTracking(eventBidId, bidder, - accountId, lineItemId, eventsContext); + final String vastUrl = eventsService.vastUrlTracking(eventBidId, bidder, accountId, eventsContext); try { return appendTrackingUrlToVastXml(vastXml, vastUrl, bidder); } catch (PreBidException e) { @@ -102,45 +109,42 @@ private static String resolveVastXmlFrom(String bidAdm, String bidNurl) { : bidAdm; } - private String appendTrackingUrlToVastXml(String vastXml, String vastUrlTracking, String bidder) { - final int inLineTagIndex = StringUtils.indexOfIgnoreCase(vastXml, IN_LINE_TAG); - final int wrapperTagIndex = StringUtils.indexOfIgnoreCase(vastXml, WRAPPER_TAG); + private static String appendTrackingUrlToVastXml(String xml, String urlTracking, String bidder) { + return appendTrackingUrl(xml, urlTracking, INLINE_OPEN_TAG_PATTERN, INLINE_CLOSE_TAG_PATTERN) + .or(() -> appendTrackingUrl(xml, urlTracking, WRAPPER_OPEN_TAG_PATTERN, WRAPPER_CLOSE_TAG_PATTERN)) + .orElseThrow(() -> new PreBidException( + "VastXml does not contain neither InLine nor Wrapper for %s response".formatted(bidder))); + } + + private static Optional appendTrackingUrl(String vastXml, + String vastUrlTracking, + Pattern openTagPattern, + Pattern closeTagPattern) { - if (inLineTagIndex != -1) { - return appendTrackingUrl(vastXml, vastUrlTracking, IN_LINE_CLOSE_TAG); - } else if (wrapperTagIndex != -1) { - return appendTrackingUrl(vastXml, vastUrlTracking, WRAPPER_CLOSE_TAG); + final Matcher openTagMatcher = openTagPattern.matcher(vastXml); + if (!openTagMatcher.find()) { + return Optional.empty(); } - throw new PreBidException("VastXml does not contain neither InLine nor Wrapper for %s response" - .formatted(bidder)); - } - private static String appendTrackingUrl(String vastXml, String vastUrlTracking, String elementCloseTag) { - if (vastXml.contains(IMPRESSION_CLOSE_TAG)) { - return insertAfterExistingImpressionTag(vastXml, vastUrlTracking); + final Matcher impressionCloseTagMatcher = IMPRESSION_CLOSE_TAG_PATTERN.matcher(vastXml); + if (impressionCloseTagMatcher.find(openTagMatcher.end())) { + int replacementEnd = impressionCloseTagMatcher.end(); + while (impressionCloseTagMatcher.find(replacementEnd)) { + replacementEnd = impressionCloseTagMatcher.end(); + } + return Optional.of(insertUrlTracking(vastXml, replacementEnd, vastUrlTracking)); } - return insertBeforeElementCloseTag(vastXml, vastUrlTracking, elementCloseTag); - } - private static String insertAfterExistingImpressionTag(String vastXml, String vastUrlTracking) { - final String impressionTag = ""; - final int replacementStart = vastXml.lastIndexOf(IMPRESSION_CLOSE_TAG); + final Matcher closeTagMatcher = closeTagPattern.matcher(vastXml); + if (!closeTagMatcher.find(openTagMatcher.end())) { + return Optional.of(vastXml); + } - return vastXml.substring(0, replacementStart) + IMPRESSION_CLOSE_TAG + impressionTag - + vastXml.substring(replacementStart + IMPRESSION_CLOSE_TAG.length()); + return Optional.of(insertUrlTracking(vastXml, closeTagMatcher.start(), vastUrlTracking)); } - private static String insertBeforeElementCloseTag(String vastXml, String vastUrlTracking, String elementCloseTag) { - final int indexOfCloseTag = StringUtils.indexOfIgnoreCase(vastXml, elementCloseTag); - - if (indexOfCloseTag == -1) { - return vastXml; - } - - final String caseSpecificCloseTag = - vastXml.substring(indexOfCloseTag, indexOfCloseTag + elementCloseTag.length()); + private static String insertUrlTracking(String vastXml, int index, String vastUrlTracking) { final String impressionTag = ""; - - return vastXml.replace(caseSpecificCloseTag, impressionTag + caseSpecificCloseTag); + return vastXml.substring(0, index) + impressionTag + vastXml.substring(index); } } diff --git a/src/main/java/org/prebid/server/vertx/CircuitBreaker.java b/src/main/java/org/prebid/server/vertx/CircuitBreaker.java index e62e3dddc98..104545470f5 100644 --- a/src/main/java/org/prebid/server/vertx/CircuitBreaker.java +++ b/src/main/java/org/prebid/server/vertx/CircuitBreaker.java @@ -6,8 +6,8 @@ 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 org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import java.time.Clock; import java.util.Objects; diff --git a/src/main/java/org/prebid/server/vertx/CloseableAdapter.java b/src/main/java/org/prebid/server/vertx/CloseableAdapter.java index 480e2e6b453..708796c63c7 100644 --- a/src/main/java/org/prebid/server/vertx/CloseableAdapter.java +++ b/src/main/java/org/prebid/server/vertx/CloseableAdapter.java @@ -1,8 +1,7 @@ package org.prebid.server.vertx; -import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; +import io.vertx.core.Promise; import java.io.Closeable; import java.io.IOException; @@ -14,19 +13,19 @@ */ public class CloseableAdapter implements io.vertx.core.Closeable { - private final Closeable adaptee; + private final Closeable closeable; - public CloseableAdapter(Closeable adaptee) { - this.adaptee = Objects.requireNonNull(adaptee); + public CloseableAdapter(Closeable closeable) { + this.closeable = Objects.requireNonNull(closeable); } @Override - public void close(Handler> completionHandler) { + public void close(Promise promise) { try { - adaptee.close(); - completionHandler.handle(Future.succeededFuture()); + closeable.close(); + promise.handle(Future.succeededFuture()); } catch (IOException e) { - completionHandler.handle(Future.failedFuture(e)); + promise.handle(Future.failedFuture(e)); } } } diff --git a/src/main/java/org/prebid/server/vertx/ContextRunner.java b/src/main/java/org/prebid/server/vertx/ContextRunner.java index ad18e40476c..43dc35c3683 100644 --- a/src/main/java/org/prebid/server/vertx/ContextRunner.java +++ b/src/main/java/org/prebid/server/vertx/ContextRunner.java @@ -1,89 +1,49 @@ package org.prebid.server.vertx; -import io.vertx.core.Context; +import io.vertx.core.Future; 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 java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; -/** - * Component that manages Vertx contexts and provides interface to run arbitrary code on them. - *

- * Needed mostly to replace verticle deployment model provided by Vertx because it doesn't play nicely when using - * Vertx in embedded mode within Spring application. - */ public class ContextRunner { - private static final Logger logger = LoggerFactory.getLogger(ContextRunner.class); - private final Vertx vertx; private final long timeoutMs; - private final Context serviceContext; - public ContextRunner(Vertx vertx, long timeoutMs) { - this.vertx = Objects.requireNonNull(vertx); + this.vertx = vertx; this.timeoutMs = timeoutMs; - - this.serviceContext = vertx.getOrCreateContext(); - } - - /** - * Runs provided action specified number of times each in a new context. This method is handy for - * running several instances of {@link io.vertx.core.http.HttpServer} on different event loop threads. - */ - public void runOnNewContext(int times, Handler> action) { - runOnContext(vertx::getOrCreateContext, times, action); - } - - /** - * Runs provided action on a dedicated service context. - */ - public void runOnServiceContext(Handler> action) { - runOnContext(() -> serviceContext, 1, action); } - private void runOnContext(Supplier contextFactory, int times, Handler> action) { - final CountDownLatch completionLatch = new CountDownLatch(times); - final AtomicBoolean actionFailed = new AtomicBoolean(false); - for (int i = 0; i < times; i++) { - final Context context = contextFactory.get(); - - final Promise promise = Promise.promise(); - promise.future().onComplete(ar -> { - if (ar.failed()) { - logger.fatal("Fatal error occurred while running action on Vertx context", ar.cause()); - actionFailed.compareAndSet(false, true); - } - completionLatch.countDown(); - }); - - context.runOnContext(v -> { - try { - action.handle(promise); - } catch (RuntimeException e) { - promise.fail(e); - } - }); - } + public void runBlocking(Handler> action) { + final CountDownLatch completionLatch = new CountDownLatch(1); + final Promise promise = Promise.promise(); + final Future future = promise.future(); + + future.onComplete(ignored -> completionLatch.countDown()); + vertx.runOnContext(v -> { + try { + action.handle(promise); + } catch (RuntimeException e) { + promise.fail(e); + } + }); try { if (!completionLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { throw new RuntimeException( "Action has not completed within defined timeout %d ms".formatted(timeoutMs)); - } else if (actionFailed.get()) { - throw new RuntimeException("Action failed"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for action to complete", e); } + + if (future.failed()) { + throw new RuntimeException(future.cause()); + } } } diff --git a/src/main/java/org/prebid/server/vertx/Initializable.java b/src/main/java/org/prebid/server/vertx/Initializable.java index 3c8bfd22c55..5c12c30e47d 100644 --- a/src/main/java/org/prebid/server/vertx/Initializable.java +++ b/src/main/java/org/prebid/server/vertx/Initializable.java @@ -1,6 +1,7 @@ package org.prebid.server.vertx; import io.vertx.core.Handler; +import io.vertx.core.Promise; /** * Denotes components requiring initialization after they have been created. @@ -11,5 +12,5 @@ @FunctionalInterface public interface Initializable { - void initialize(); + void initialize(Promise initializePromise); } diff --git a/src/main/java/org/prebid/server/vertx/LocalMessageCodec.java b/src/main/java/org/prebid/server/vertx/LocalMessageCodec.java deleted file mode 100644 index 302dcb11eff..00000000000 --- a/src/main/java/org/prebid/server/vertx/LocalMessageCodec.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.prebid.server.vertx; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.eventbus.EventBus; -import io.vertx.core.eventbus.MessageCodec; - -/** - * Message codec intended for use with objects passed around via {@link EventBus} only locally, i.e. within one JVM. - */ -public class LocalMessageCodec implements MessageCodec { - - private static final String CODEC_NAME = "LocalMessageCodec"; - - public static MessageCodec create() { - return new LocalMessageCodec(); - } - - @Override - public void encodeToWire(Buffer buffer, Object source) { - throw new UnsupportedOperationException("Serialization is not supported by this message codec"); - } - - @Override - public Object decodeFromWire(int pos, Buffer buffer) { - throw new UnsupportedOperationException("Deserialization is not supported by this message codec"); - } - - @Override - public Object transform(Object source) { - return source; - } - - @Override - public String name() { - return codecName(); - } - - @Override - public byte systemCodecID() { - return -1; - } - - public static String codecName() { - return CODEC_NAME; - } -} diff --git a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java new file mode 100644 index 00000000000..e5aa90aabb2 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java @@ -0,0 +1,94 @@ +package org.prebid.server.vertx.database; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.Tuple; +import org.prebid.server.execution.Timeout; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.Metrics; + +import java.time.Clock; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +/** + * Wrapper over {@link Pool} that supports setting query timeout in milliseconds. + */ +public class BasicDatabaseClient implements DatabaseClient { + + private static final Logger logger = LoggerFactory.getLogger(BasicDatabaseClient.class); + + private final Pool pool; + private final Metrics metrics; + private final Clock clock; + + public BasicDatabaseClient(Pool pool, Metrics metrics, Clock clock) { + this.pool = Objects.requireNonNull(pool); + this.metrics = Objects.requireNonNull(metrics); + this.clock = Objects.requireNonNull(clock); + } + + /** + * Triggers connection creation. Should be called during application initialization to detect connection issues as + * early as possible. + *

+ * Must be called on Vertx event loop thread. + */ + public Future initialize() { + return pool.getConnection() + .recover(BasicDatabaseClient::logConnectionError) + .mapEmpty(); + } + + @Override + public Future executeQuery(String query, + List params, + Function, T> mapper, + Timeout timeout) { + + final long remainingTimeout = timeout.remaining(); + if (remainingTimeout <= 0) { + return Future.failedFuture(timeoutException()); + } + final long startTime = clock.millis(); + + return pool.getConnection() + .recover(BasicDatabaseClient::logConnectionError) + .compose(connection -> makeQuery(connection, query, params)) + .timeout(remainingTimeout, TimeUnit.MILLISECONDS) + .recover(this::handleFailure) + .onComplete(result -> metrics.updateDatabaseQueryTimeMetric(clock.millis() - startTime)) + .map(mapper); + } + + private Future> handleFailure(Throwable throwable) { + if (throwable instanceof TimeoutException) { + return Future.failedFuture(timeoutException()); + } + + return Future.failedFuture(throwable); + } + + private static Future logConnectionError(Throwable exception) { + logger.warn("Cannot connect to database", exception); + return Future.failedFuture(exception); + } + + /** + * Performs query to DB. + */ + private static Future> makeQuery(SqlConnection connection, String query, List params) { + return connection.preparedQuery(query).execute(Tuple.tuple(params)).onComplete(ignored -> connection.close()); + } + + private static TimeoutException timeoutException() { + return new TimeoutException("Timed out while executing SQL query"); + } +} diff --git a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java new file mode 100644 index 00000000000..bb4fa7d09c1 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java @@ -0,0 +1,79 @@ +package org.prebid.server.vertx.database; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.prebid.server.execution.Timeout; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.CircuitBreaker; + +import java.time.Clock; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * Database Client wrapped by {@link CircuitBreaker} to achieve robust operating. + */ +public class CircuitBreakerSecuredDatabaseClient implements DatabaseClient { + + private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerSecuredDatabaseClient.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final int LOG_PERIOD_SECONDS = 5; + + private final DatabaseClient databaseClient; + private final CircuitBreaker breaker; + + public CircuitBreakerSecuredDatabaseClient(Vertx vertx, + DatabaseClient databaseClient, + Metrics metrics, + int openingThreshold, + long openingIntervalMs, + long closingIntervalMs, + Clock clock) { + + this.databaseClient = Objects.requireNonNull(databaseClient); + + breaker = new CircuitBreaker( + "db_cb", + Objects.requireNonNull(vertx), + openingThreshold, + openingIntervalMs, + closingIntervalMs, + Objects.requireNonNull(clock)) + .openHandler(ignored -> circuitOpened()) + .halfOpenHandler(ignored -> circuitHalfOpened()) + .closeHandler(ignored -> circuitClosed()); + + metrics.createDatabaseCircuitBreakerGauge(breaker::isOpen); + + logger.info("Initialized database client with Circuit Breaker"); + } + + @Override + public Future executeQuery(String query, + List params, + Function, T> mapper, + Timeout timeout) { + + return breaker.execute( + promise -> databaseClient.executeQuery(query, params, mapper, timeout).onComplete(promise)); + } + + private void circuitOpened() { + conditionalLogger.warn("Database is unavailable, circuit opened.", LOG_PERIOD_SECONDS, TimeUnit.SECONDS); + } + + private void circuitHalfOpened() { + logger.warn("Database is ready to try again, circuit half-opened."); + } + + private void circuitClosed() { + logger.warn("Database becomes working, circuit closed."); + } +} diff --git a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java new file mode 100644 index 00000000000..87c9ada84c6 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java @@ -0,0 +1,22 @@ +package org.prebid.server.vertx.database; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import org.prebid.server.execution.Timeout; + +import java.util.List; +import java.util.function.Function; + +/** + * Interface for asynchronous interaction with database over database API. + */ +@FunctionalInterface +public interface DatabaseClient { + + /** + * Executes query with parameters and returns {@link Future} eventually holding result mapped to a model + * object by provided mapper. + */ + Future executeQuery(String query, List params, Function, T> mapper, Timeout timeout); +} diff --git a/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java b/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java deleted file mode 100644 index dfeb5c4a32e..00000000000 --- a/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.prebid.server.vertx.http; - -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpMethod; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.util.Objects; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; - -/** - * Simple wrapper around {@link HttpClient} with general functionality. - */ -public class BasicHttpClient implements HttpClient { - - private final Vertx vertx; - private final io.vertx.core.http.HttpClient httpClient; - - public BasicHttpClient(Vertx vertx, io.vertx.core.http.HttpClient httpClient) { - this.vertx = Objects.requireNonNull(vertx); - this.httpClient = Objects.requireNonNull(httpClient); - } - - @Override - public Future request(HttpMethod method, String url, MultiMap headers, - String body, long timeoutMs, long maxResponseSize) { - return request(method, url, headers, timeoutMs, maxResponseSize, body, - (HttpClientRequest httpClientRequest) -> httpClientRequest.end(body)); - } - - @Override - public Future request(HttpMethod method, String url, MultiMap headers, - byte[] body, long timeoutMs, long maxResponseSize) { - return request(method, url, headers, timeoutMs, maxResponseSize, body, - (HttpClientRequest httpClientRequest) -> httpClientRequest.end(Buffer.buffer(body))); - } - - private Future request(HttpMethod method, String url, MultiMap headers, - long timeoutMs, long maxResponseSize, T body, - Consumer requestBodySetter) { - final Promise promise = Promise.promise(); - - if (timeoutMs <= 0) { - failResponse(new TimeoutException("Timeout has been exceeded"), promise); - } else { - final HttpClientRequest httpClientRequest; - try { - httpClientRequest = httpClient.requestAbs(method, url); - } catch (Exception e) { - failResponse(e, promise); - return promise.future(); - } - - // Vert.x HttpClientRequest timeout doesn't aware of case when a part of the response body is received, - // but remaining part is delayed. So, overall request/response timeout is involved to fix it. - final long timerId = vertx.setTimer(timeoutMs, id -> handleTimeout(promise, timeoutMs, httpClientRequest)); - - httpClientRequest - .setFollowRedirects(true) - .handler(response -> handleResponse(response, promise, timerId, maxResponseSize)) - .exceptionHandler(exception -> failResponse(exception, promise, timerId)); - - if (headers != null) { - httpClientRequest.headers().addAll(headers); - } - - if (body != null) { - requestBodySetter.accept(httpClientRequest); - } else { - httpClientRequest.end(); - } - } - return promise.future(); - } - - private void handleTimeout(Promise promise, - long timeoutMs, - HttpClientRequest httpClientRequest) { - - if (!promise.future().isComplete()) { - failResponse( - new TimeoutException("Timeout period of %dms has been exceeded".formatted(timeoutMs)), promise); - - // Explicitly close connection, inspired by https://github.com/eclipse-vertx/vert.x/issues/2745 - httpClientRequest.reset(); - } - } - - private void handleResponse(io.vertx.core.http.HttpClientResponse response, - Promise promise, long timerId, long maxResponseSize) { - final String contentLength = response.getHeader(HttpHeaders.CONTENT_LENGTH); - final long responseBodySize = contentLength != null ? Long.parseLong(contentLength) : 0; - if (responseBodySize > maxResponseSize) { - failResponse( - new PreBidException( - "Response size %d exceeded %d bytes limit".formatted(responseBodySize, maxResponseSize)), - promise, - timerId); - return; - } - - response - .bodyHandler(buffer -> successResponse(buffer.toString(), response, promise, timerId)) - .exceptionHandler(exception -> failResponse(exception, promise, timerId)); - } - - private void successResponse(String body, io.vertx.core.http.HttpClientResponse response, - Promise promise, long timerId) { - vertx.cancelTimer(timerId); - - promise.tryComplete(HttpClientResponse.of(response.statusCode(), response.headers(), body)); - } - - private void failResponse(Throwable exception, Promise promise, long timerId) { - vertx.cancelTimer(timerId); - - failResponse(exception, promise); - } - - private static void failResponse(Throwable exception, Promise promise) { - promise.tryFail(exception); - } -} diff --git a/src/main/java/org/prebid/server/vertx/httpclient/BasicHttpClient.java b/src/main/java/org/prebid/server/vertx/httpclient/BasicHttpClient.java new file mode 100644 index 00000000000..35dd808c97d --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/httpclient/BasicHttpClient.java @@ -0,0 +1,112 @@ +package org.prebid.server.vertx.httpclient; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +/** + * Simple wrapper around {@link HttpClient} with general functionality. + */ +public class BasicHttpClient implements HttpClient { + + private final Vertx vertx; + private final io.vertx.core.http.HttpClient httpClient; + + public BasicHttpClient(Vertx vertx, io.vertx.core.http.HttpClient httpClient) { + this.vertx = Objects.requireNonNull(vertx); + this.httpClient = Objects.requireNonNull(httpClient); + } + + @Override + public Future request(HttpMethod method, String url, MultiMap headers, + String body, long timeoutMs, long maxResponseSize) { + + return request(method, url, headers, timeoutMs, maxResponseSize, body != null ? body.getBytes() : null); + } + + @Override + public Future request(HttpMethod method, String url, MultiMap headers, + byte[] body, long timeoutMs, long maxResponseSize) { + + return request(method, url, headers, timeoutMs, maxResponseSize, body); + } + + private Future request(HttpMethod method, String url, MultiMap headers, + long timeoutMs, long maxResponseSize, byte[] body) { + + if (timeoutMs <= 0) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final URL absoluteUrl; + try { + absoluteUrl = new URL(url); + } catch (MalformedURLException e) { + return Future.failedFuture(e); + } + + final Promise responsePromise = Promise.promise(); + final long timerId = vertx.setTimer(timeoutMs, ignored -> + responsePromise.tryFail( + new TimeoutException("Timeout period of %dms has been exceeded".formatted(timeoutMs)))); + + final RequestOptions options = new RequestOptions() + .setFollowRedirects(true) + .setConnectTimeout(timeoutMs) + .setMethod(method) + .setAbsoluteURI(absoluteUrl) + .setHeaders(headers); + + final Future requestFuture = makeRequest(options); + + requestFuture + .compose(request -> body != null ? request.send(Buffer.buffer(body)) : request.send()) + .compose(response -> toInternalResponse(response, maxResponseSize)) + .onSuccess(responsePromise::tryComplete) + .onFailure(responsePromise::tryFail); + + return responsePromise.future() + .onComplete(ignored -> vertx.cancelTimer(timerId)) + .onFailure(ignored -> requestFuture.onSuccess(HttpClientRequest::reset)); + } + + private Future makeRequest(RequestOptions options) { + try { + return httpClient.request(options); + } catch (Throwable e) { + return Future.failedFuture(e); + } + } + + private Future toInternalResponse(io.vertx.core.http.HttpClientResponse response, + long maxResponseSize) { + + final String contentLength = response.getHeader(HttpHeaders.CONTENT_LENGTH); + final long responseBodySize = contentLength != null ? Long.parseLong(contentLength) : 0; + if (responseBodySize > maxResponseSize) { + return Future.failedFuture(new PreBidException( + "Response size %d exceeded %d bytes limit".formatted(responseBodySize, maxResponseSize))); + } + + return response.body() + .map(body -> HttpClientResponse.of( + response.statusCode(), + response.headers(), + body.toString(StandardCharsets.UTF_8))); + + } +} diff --git a/src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java b/src/main/java/org/prebid/server/vertx/httpclient/CircuitBreakerSecuredHttpClient.java similarity index 94% rename from src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java rename to src/main/java/org/prebid/server/vertx/httpclient/CircuitBreakerSecuredHttpClient.java index 767398b37ad..0843a04de12 100644 --- a/src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java +++ b/src/main/java/org/prebid/server/vertx/httpclient/CircuitBreakerSecuredHttpClient.java @@ -1,17 +1,17 @@ -package org.prebid.server.vertx.http; +package org.prebid.server.vertx.httpclient; import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.prebid.server.exception.PreBidException; import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.vertx.CircuitBreaker; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.net.MalformedURLException; import java.net.URL; @@ -127,11 +127,11 @@ private void circuitOpened(String name) { } private void circuitHalfOpened(String name) { - logger.warn("Http client request to {0} will try again, circuit half-opened.", name); + logger.warn("Http client request to {} will try again, circuit half-opened.", name); } private void circuitClosed(String name) { - logger.warn("Http client request to {0} becomes succeeded, circuit closed.", name); + logger.warn("Http client request to {} becomes succeeded, circuit closed.", name); } private static String nameFrom(String urlAsString) { diff --git a/src/main/java/org/prebid/server/vertx/http/HttpClient.java b/src/main/java/org/prebid/server/vertx/httpclient/HttpClient.java similarity index 94% rename from src/main/java/org/prebid/server/vertx/http/HttpClient.java rename to src/main/java/org/prebid/server/vertx/httpclient/HttpClient.java index d448f58b4bb..467cd43157c 100644 --- a/src/main/java/org/prebid/server/vertx/http/HttpClient.java +++ b/src/main/java/org/prebid/server/vertx/httpclient/HttpClient.java @@ -1,9 +1,9 @@ -package org.prebid.server.vertx.http; +package org.prebid.server.vertx.httpclient; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; -import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; /** * Interface describes HTTP interactions. diff --git a/src/main/java/org/prebid/server/vertx/http/model/HttpClientResponse.java b/src/main/java/org/prebid/server/vertx/httpclient/model/HttpClientResponse.java similarity index 87% rename from src/main/java/org/prebid/server/vertx/http/model/HttpClientResponse.java rename to src/main/java/org/prebid/server/vertx/httpclient/model/HttpClientResponse.java index 654d987d8b4..7ed9056e62e 100644 --- a/src/main/java/org/prebid/server/vertx/http/model/HttpClientResponse.java +++ b/src/main/java/org/prebid/server/vertx/httpclient/model/HttpClientResponse.java @@ -1,4 +1,4 @@ -package org.prebid.server.vertx.http.model; +package org.prebid.server.vertx.httpclient.model; import io.vertx.core.MultiMap; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/prebid/server/vertx/jdbc/BasicJdbcClient.java b/src/main/java/org/prebid/server/vertx/jdbc/BasicJdbcClient.java deleted file mode 100644 index 71fd24bd166..00000000000 --- a/src/main/java/org/prebid/server/vertx/jdbc/BasicJdbcClient.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.prebid.server.vertx.jdbc; - -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonArray; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.jdbc.JDBCClient; -import io.vertx.ext.sql.ResultSet; -import io.vertx.ext.sql.SQLConnection; -import org.prebid.server.execution.Timeout; -import org.prebid.server.metric.Metrics; - -import java.time.Clock; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; - -/** - * Wrapper over {@link JDBCClient} that supports setting query timeout in milliseconds. - */ -public class BasicJdbcClient implements JdbcClient { - - private static final Logger logger = LoggerFactory.getLogger(BasicJdbcClient.class); - - private final Vertx vertx; - private final JDBCClient jdbcClient; - private final Metrics metrics; - private final Clock clock; - - public BasicJdbcClient(Vertx vertx, JDBCClient jdbcClient, Metrics metrics, Clock clock) { - this.vertx = Objects.requireNonNull(vertx); - this.jdbcClient = Objects.requireNonNull(jdbcClient); - this.metrics = Objects.requireNonNull(metrics); - this.clock = Objects.requireNonNull(clock); - } - - /** - * Triggers connection creation. Should be called during application initialization to detect connection issues as - * early as possible. - *

- * Must be called on Vertx event loop thread. - */ - public Future initialize() { - final Promise connectionPromise = Promise.promise(); - jdbcClient.getConnection(connectionPromise); - return connectionPromise.future() - .recover(BasicJdbcClient::logConnectionError) - .mapEmpty(); - } - - @Override - public Future executeQuery(String query, List params, Function mapper, - Timeout timeout) { - final long remainingTimeout = timeout.remaining(); - if (remainingTimeout <= 0) { - return Future.failedFuture(timeoutException()); - } - final long startTime = clock.millis(); - final Promise queryResultPromise = Promise.promise(); - - // timeout implementation is inspired by this answer: - // https://groups.google.com/d/msg/vertx/eSf3AQagGGU/K7pztnjLc_EJ - final long timerId = vertx.setTimer(remainingTimeout, id -> timedOutResult(queryResultPromise, startTime)); - - final Promise connectionPromise = Promise.promise(); - jdbcClient.getConnection(connectionPromise); - connectionPromise.future() - .recover(BasicJdbcClient::logConnectionError) - .compose(connection -> makeQuery(connection, query, params)) - .onComplete(result -> handleResult(result, queryResultPromise, timerId, startTime)); - - return queryResultPromise.future().map(mapper); - } - - /** - * Fails result {@link Promise} with timeout exception. - */ - private void timedOutResult(Promise queryResultPromise, long startTime) { - // no need for synchronization since timer is fired on the same event loop thread - if (!queryResultPromise.future().isComplete()) { - metrics.updateDatabaseQueryTimeMetric(clock.millis() - startTime); - queryResultPromise.fail(timeoutException()); - } - } - - private static Future logConnectionError(Throwable exception) { - logger.warn("Cannot connect to database", exception); - return Future.failedFuture(exception); - } - - /** - * Performs query to DB. - */ - private static Future makeQuery(SQLConnection connection, String query, List params) { - final Promise resultSetPromise = Promise.promise(); - connection.queryWithParams(query, new JsonArray(params), - ar -> { - connection.close(); - resultSetPromise.handle(ar); - }); - return resultSetPromise.future(); - } - - /** - * Propagates responded {@link ResultSet} (or failure) to result {@link Promise}. - */ - private void handleResult( - AsyncResult result, Promise queryResultPromise, long timerId, long startTime) { - - vertx.cancelTimer(timerId); - - // check is to avoid harmless exception if timeout exceeds before successful result becomes ready - if (!queryResultPromise.future().isComplete()) { - metrics.updateDatabaseQueryTimeMetric(clock.millis() - startTime); - queryResultPromise.handle(result); - } - } - - private static TimeoutException timeoutException() { - return new TimeoutException("Timed out while executing SQL query"); - } -} diff --git a/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java b/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java deleted file mode 100644 index be662e057c9..00000000000 --- a/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.prebid.server.vertx.jdbc; - -import io.vertx.core.Future; -import io.vertx.core.Vertx; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.sql.ResultSet; -import org.prebid.server.execution.Timeout; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.metric.Metrics; -import org.prebid.server.vertx.CircuitBreaker; - -import java.time.Clock; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -/** - * JDBC Client wrapped by {@link CircuitBreaker} to achieve robust operating. - */ -public class CircuitBreakerSecuredJdbcClient implements JdbcClient { - - private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerSecuredJdbcClient.class); - private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); - private static final int LOG_PERIOD_SECONDS = 5; - - private final JdbcClient jdbcClient; - private final CircuitBreaker breaker; - - public CircuitBreakerSecuredJdbcClient(Vertx vertx, - JdbcClient jdbcClient, - Metrics metrics, - int openingThreshold, - long openingIntervalMs, - long closingIntervalMs, - Clock clock) { - - this.jdbcClient = Objects.requireNonNull(jdbcClient); - - breaker = new CircuitBreaker( - "db_cb", - Objects.requireNonNull(vertx), - openingThreshold, - openingIntervalMs, - closingIntervalMs, - Objects.requireNonNull(clock)) - .openHandler(ignored -> circuitOpened()) - .halfOpenHandler(ignored -> circuitHalfOpened()) - .closeHandler(ignored -> circuitClosed()); - - metrics.createDatabaseCircuitBreakerGauge(breaker::isOpen); - - logger.info("Initialized JDBC client with Circuit Breaker"); - } - - @Override - public Future executeQuery(String query, - List params, - Function mapper, - Timeout timeout) { - - return breaker.execute(promise -> jdbcClient.executeQuery(query, params, mapper, timeout).onComplete(promise)); - } - - private void circuitOpened() { - conditionalLogger.warn("Database is unavailable, circuit opened.", LOG_PERIOD_SECONDS, TimeUnit.SECONDS); - } - - private void circuitHalfOpened() { - logger.warn("Database is ready to try again, circuit half-opened."); - } - - private void circuitClosed() { - logger.warn("Database becomes working, circuit closed."); - } -} diff --git a/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java b/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java deleted file mode 100644 index b447ea98dc9..00000000000 --- a/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.vertx.jdbc; - -import io.vertx.core.Future; -import io.vertx.ext.sql.ResultSet; -import org.prebid.server.execution.Timeout; - -import java.util.List; -import java.util.function.Function; - -/** - * Interface for asynchronous interaction with database over JDBC API. - */ -@FunctionalInterface -public interface JdbcClient { - - /** - * Executes query with parameters and returns {@link Future} eventually holding result mapped to a model - * object by provided mapper. - */ - Future executeQuery(String query, List params, Function mapper, Timeout timeout); -} diff --git a/src/main/java/org/prebid/server/vertx/verticles/VerticleDefinition.java b/src/main/java/org/prebid/server/vertx/verticles/VerticleDefinition.java new file mode 100644 index 00000000000..52334bf3c97 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/verticles/VerticleDefinition.java @@ -0,0 +1,22 @@ +package org.prebid.server.vertx.verticles; + +import io.vertx.core.Verticle; +import lombok.Value; + +import java.util.function.Supplier; + +@Value(staticConstructor = "of") +public class VerticleDefinition { + + Supplier factory; + + int amount; + + public static VerticleDefinition ofSingleInstance(Supplier factory) { + return of(factory, 1); + } + + public static VerticleDefinition ofMultiInstance(Supplier factory, int amount) { + return of(factory, amount); + } +} diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java b/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java new file mode 100644 index 00000000000..c6175ccaafa --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java @@ -0,0 +1,63 @@ +package org.prebid.server.vertx.verticles.server; + +import com.codahale.metrics.ScheduledReporter; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Closeable; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.vertx.CloseableAdapter; +import org.prebid.server.vertx.Initializable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +public class DaemonVerticle extends AbstractVerticle { + + private static final Logger logger = LoggerFactory.getLogger(DaemonVerticle.class); + + private final List initializables; + private final List closeables; + + public DaemonVerticle(List initializables, List reporters) { + this.initializables = ListUtils.emptyIfNull(initializables); + this.closeables = ListUtils.emptyIfNull(reporters).stream() + .map(CloseableAdapter::new) + .toList(); + } + + @Override + public void start(Promise startPromise) { + all(initializables, initializable -> initializable::initialize).onComplete(startPromise); + } + + @Override + public void stop(Promise stopPromise) { + all(closeables, closeable -> closeable::close).onComplete(stopPromise); + } + + private static Future all(Collection entries, + Function>> entryToPromiseConsumerMapper) { + + final List> entriesFutures = new ArrayList<>(); + + for (E entry : entries) { + final Promise entryPromise = Promise.promise(); + entriesFutures.add(entryPromise.future()); + + entryToPromiseConsumerMapper.apply(entry).accept(entryPromise); + } + + return Future.all(entriesFutures) + .onSuccess(r -> logger.info( + "Successfully started {} instance on thread: {}", + DaemonVerticle.class.getSimpleName(), + Thread.currentThread().getName())) + .mapEmpty(); + } +} diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/HttpEndpoint.java b/src/main/java/org/prebid/server/vertx/verticles/server/HttpEndpoint.java new file mode 100644 index 00000000000..811f6a1aa1a --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/verticles/server/HttpEndpoint.java @@ -0,0 +1,12 @@ +package org.prebid.server.vertx.verticles.server; + +import io.vertx.core.http.HttpMethod; +import lombok.Value; + +@Value(staticConstructor = "of") +public class HttpEndpoint { + + HttpMethod method; + + String path; +} diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/ServerVerticle.java b/src/main/java/org/prebid/server/vertx/verticles/server/ServerVerticle.java new file mode 100644 index 00000000000..147de2210c4 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/verticles/server/ServerVerticle.java @@ -0,0 +1,73 @@ +package org.prebid.server.vertx.verticles.server; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.AsyncResult; +import io.vertx.core.Promise; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.Router; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.handler.ExceptionHandler; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; + +public class ServerVerticle extends AbstractVerticle { + + private static final Logger logger = LoggerFactory.getLogger(ServerVerticle.class); + + private final String name; + private final HttpServerOptions serverOptions; + private final SocketAddress address; + private final Router router; + private final ExceptionHandler exceptionHandler; + + public ServerVerticle(String name, + HttpServerOptions serverOptions, + SocketAddress address, + Router router, + ExceptionHandler exceptionHandler) { + + this.name = Objects.requireNonNull(name); + this.serverOptions = Objects.requireNonNull(serverOptions); + this.address = Objects.requireNonNull(address); + this.router = Objects.requireNonNull(router); + this.exceptionHandler = Objects.requireNonNull(exceptionHandler); + } + + public ServerVerticle(String name, SocketAddress address, Router router) { + this.name = Objects.requireNonNull(name); + this.serverOptions = null; + this.address = Objects.requireNonNull(address); + this.router = Objects.requireNonNull(router); + this.exceptionHandler = null; + } + + @Override + public void start(Promise startPromise) { + final HttpServerOptions httpServerOptions = ObjectUtils.getIfNull(serverOptions, HttpServerOptions::new); + final HttpServer server = vertx.createHttpServer(httpServerOptions) + .requestHandler(router); + + if (exceptionHandler != null) { + server.exceptionHandler(exceptionHandler); + } + + server.listen(address, result -> onServerStarted(result, startPromise)); + } + + private void onServerStarted(AsyncResult result, Promise startPromise) { + if (result.succeeded()) { + startPromise.tryComplete(); + logger.info( + "Successfully started {} instance on address: {}, thread: {}", + name, + address, + Thread.currentThread().getName()); + } else { + startPromise.tryFail(result.cause()); + } + } +} diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/admin/AdminResource.java b/src/main/java/org/prebid/server/vertx/verticles/server/admin/AdminResource.java new file mode 100644 index 00000000000..0f901ec1167 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/verticles/server/admin/AdminResource.java @@ -0,0 +1,13 @@ +package org.prebid.server.vertx.verticles.server.admin; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public interface AdminResource extends Handler { + + String path(); + + boolean isOnApplicationPort(); + + boolean isSecured(); +} diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/application/ApplicationResource.java b/src/main/java/org/prebid/server/vertx/verticles/server/application/ApplicationResource.java new file mode 100644 index 00000000000..ee352dc9c99 --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/verticles/server/application/ApplicationResource.java @@ -0,0 +1,12 @@ +package org.prebid.server.vertx.verticles.server.application; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; + +import java.util.List; + +public interface ApplicationResource extends Handler { + + List endpoints(); +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 14b64b0cb1e..eb73cc16478 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -67,26 +67,6 @@ admin-endpoints: path: /pbs-admin/tracelog on-application-port: false protected: true - deals-status: - enabled: false - path: /pbs-admin/deals-status - on-application-port: false - protected: true - lineitem-status: - enabled: false - path: /pbs-admin/lineitem-status - on-application-port: false - protected: true - force-deals-update: - enabled: false - path: /pbs-admin/force-deals-update - on-application-port: false - protected: true - e2eadmin: - enabled: false - path: /pbs-admin/e2eAdmin/* - on-application-port: false - protected: true collected-metrics: enabled: false path: /collected-metrics @@ -122,14 +102,14 @@ adapter-defaults: allow: true auction: ad-server-currency: USD - blacklisted-accounts: - blacklisted-apps: + blocklisted-accounts: + blocklisted-apps: biddertmax: min: 50 max: 5000 percent: 100 tmax-upstream-response-time: 30 - stored-requests-timeout-ms: 50 + stored-requests-timeout-ms: 100 timeout-notification: timeout-ms: 200 log-result: false @@ -145,7 +125,7 @@ auction: secure-markup: skip host-schain-node: category-mapping-enabled: false - strict-app-site-dooh: false + strict-app-site-dooh: true video: stored-request-required: false stored-requests-timeout-ms: 90 @@ -180,7 +160,9 @@ settings: enforce-valid-account: false database: pool-size: 20 - provider-class: c3p0 + idle-connection-timeout: 300 + enable-prepared-statement-caching: false + max-prepared-statement-cache-size: 256 targeting: truncate-attr-chars: 20 default-account-config: > @@ -301,6 +283,8 @@ ipv6: anon-left-mask-bits: 56 private-networks: ::1/128, 2001:db8::/32, fc00::/7, fe80::/10, ff00::/8 analytics: + global: + adapters: logAnalytics, pubstack, greenbids, agmaAnalytics pubstack: enabled: false endpoint: http://localhost:8090 @@ -311,42 +295,22 @@ analytics: size-bytes: 2097152 count: 100 report-ttl-ms: 900000 - -device-info: - enabled: false -deals: - enabled: false - simulation: + greenbids: + analytics-server-version: "2.2.0" + analytics-server: http://localhost:8090 + exploratory-sampling-split: 0.9 + timeout-ms: 10000 + agma: enabled: false - ready-at-adjustment-ms: 0 - planner: - plan-advance-period: "0 */1 * * * *" - update-period: "0 */1 * * * *" - timeout-ms: 4000 - register-period-sec: 60 - delivery-stats: - delivery-period: "0 */1 * * * *" - cached-reports-number: 20 - line-item-status-ttl-sec: 3600 - line-items-per-report: 25 - reports-interval-ms: 0 - batches-interval-ms: 1000 - request-compression-enabled: true - delivery-progress: - line-item-status-ttl-sec: 3600 - cached-plans-number: 20 - report-reset-period: "0 */1 * * * *" - delivery-progress-report: - competitors-number: 10 - max-deals-per-bidder: 3 - alert-proxy: - enabled: false - url: http://localhost - timeout-sec: 5 - alert-types: - pbs-planner-client-error: 15 - pbs-planner-empty-response-error: 15 - pbs-register-client-error: 15 - pbs-delivery-stats-client-error: 15 + accounts: + - code: code + publisher-id: pub + buffers: + size-bytes: 100000 + timeout-ms: 5000 + count: 4 + endpoint: + url: http:/url.com + timeout-ms: 5000 price-floors: enabled: false diff --git a/src/main/resources/bidder-config/aax.yaml b/src/main/resources/bidder-config/aax.yaml index b83b0a9bcf6..b695864b8c2 100644 --- a/src/main/resources/bidder-config/aax.yaml +++ b/src/main/resources/bidder-config/aax.yaml @@ -1,7 +1,7 @@ adapters: aax: endpoint: https://prebid.aaxads.com/rtb/pb/aax-prebid?src={{PREBID_SERVER_ENDPOINT}} - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: product@aax.media app-media-types: diff --git a/src/main/resources/bidder-config/adf.yaml b/src/main/resources/bidder-config/adf.yaml index ef554038170..738b737c11c 100644 --- a/src/main/resources/bidder-config/adf.yaml +++ b/src/main/resources/bidder-config/adf.yaml @@ -18,6 +18,6 @@ adapters: usersync: cookie-family-name: adf redirect: - url: https://cm.adform.net/cookie?redirect_url={{redirect_url}} + url: https://c1.adform.net/cookie?redirect_url={{redirect_url}} support-cors: false uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/adkernel.yaml b/src/main/resources/bidder-config/adkernel.yaml index 210900b4466..8fe685f93e2 100644 --- a/src/main/resources/bidder-config/adkernel.yaml +++ b/src/main/resources/bidder-config/adkernel.yaml @@ -1,6 +1,6 @@ adapters: adkernel: - endpoint: https://pbs.adksrv.com/hb?zone=%s + endpoint: http://pbs.adksrv.com/hb?zone=%s endpoint-compression: gzip meta-info: maintainer-email: prebid-dev@adkernel.com diff --git a/src/main/resources/bidder-config/admatic.yaml b/src/main/resources/bidder-config/admatic.yaml new file mode 100644 index 00000000000..52dfd41246a --- /dev/null +++ b/src/main/resources/bidder-config/admatic.yaml @@ -0,0 +1,15 @@ +adapters: + admatic: + endpoint: http://pbs.admatic.com.tr?host={{Host}} + meta-info: + maintainer-email: prebid@admatic.com.tr + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1281 diff --git a/src/main/resources/bidder-config/adot.yaml b/src/main/resources/bidder-config/adot.yaml index 91a975e521f..fa4a5a5f489 100644 --- a/src/main/resources/bidder-config/adot.yaml +++ b/src/main/resources/bidder-config/adot.yaml @@ -1,6 +1,6 @@ adapters: adot: - endpoint: https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest + endpoint: https://dsp.adotmob.com/headerbidding{{PUBLISHER_PATH}}/bidrequest meta-info: maintainer-email: admin@we-are-adot.com app-media-types: diff --git a/src/main/resources/bidder-config/adprime.yaml b/src/main/resources/bidder-config/adprime.yaml index 92306c86c4d..7ff74879e01 100644 --- a/src/main/resources/bidder-config/adprime.yaml +++ b/src/main/resources/bidder-config/adprime.yaml @@ -13,3 +13,13 @@ adapters: - native supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: adprime + iframe: + url: https://sync.adprime.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + redirect: + url: https://sync.adprime.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/adrino.yaml b/src/main/resources/bidder-config/adrino.yaml deleted file mode 100644 index 122130ecbbd..00000000000 --- a/src/main/resources/bidder-config/adrino.yaml +++ /dev/null @@ -1,10 +0,0 @@ -adapters: - adrino: - endpoint: https://prd-prebid-bidder.adrino.io/openrtb/bid - meta-info: - maintainer-email: dev@adrino.pl - app-media-types: - site-media-types: - - native - supported-vendors: - vendor-id: 1072 diff --git a/src/main/resources/bidder-config/adtonos.yaml b/src/main/resources/bidder-config/adtonos.yaml new file mode 100644 index 00000000000..e1a19fbc6eb --- /dev/null +++ b/src/main/resources/bidder-config/adtonos.yaml @@ -0,0 +1,22 @@ +adapters: + adtonos: + endpoint: https://exchange.adtonos.com/bid/{{PublisherId}} + geoscope: + - global + meta-info: + maintainer-email: support@adtonos.com + app-media-types: + - video + - audio + site-media-types: + - audio + dooh-media-types: + - audio + supported-vendors: + vendor-id: 682 + usersync: + cookie-family-name: adtonos + redirect: + url: https://play.adtonos.com/redir?to={{redirect_url}} + support-cors: false + uid-macro: '@UUID@' diff --git a/src/main/resources/bidder-config/aidem.yaml b/src/main/resources/bidder-config/aidem.yaml index cd8b37239ab..9cd7c5432af 100644 --- a/src/main/resources/bidder-config/aidem.yaml +++ b/src/main/resources/bidder-config/aidem.yaml @@ -1,7 +1,7 @@ adapters: aidem: endpoint: https://zero.aidemsrv.com/ortb/v2.6/bid/request?billing_id={{PublisherId}} - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: prebid@aidem.com app-media-types: diff --git a/src/main/resources/bidder-config/algorix.yaml b/src/main/resources/bidder-config/algorix.yaml index 70c56825fdd..f76db6420ce 100644 --- a/src/main/resources/bidder-config/algorix.yaml +++ b/src/main/resources/bidder-config/algorix.yaml @@ -1,6 +1,6 @@ adapters: algorix: - endpoint: https://{HOST}.svr-algorix.com/rtb/sa?sid={SID}&token={TOKEN} + endpoint: https://{{HOST}}.svr-algorix.com/rtb/sa?sid={{SID}}&token={{TOKEN}} meta-info: maintainer-email: prebid@algorix.co app-media-types: diff --git a/src/main/resources/bidder-config/aso.yaml b/src/main/resources/bidder-config/aso.yaml new file mode 100644 index 00000000000..ddae491f357 --- /dev/null +++ b/src/main/resources/bidder-config/aso.yaml @@ -0,0 +1,25 @@ +adapters: + aso: + endpoint: https://srv.aso1.net/pbs/bidder?zid={{ZoneID}} + aliases: + bcmint: + enabled: false + endpoint: https://srv.datacygnal.io/pbs/bidder?zid={{ZoneID}} + meta-info: + maintainer-email: contact@bcm.ltd + bidagency: + enabled: false + endpoint: https://srv.bidgx.com/pbs/bidder?zid={{ZoneID}} + meta-info: + maintainer-email: aso@bidgency.com + meta-info: + maintainer-email: support@adsrv.org + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + vendor-id: 0 diff --git a/src/main/resources/bidder-config/axonix.yaml b/src/main/resources/bidder-config/axonix.yaml index c0b6e12ada7..e8dd2352cbe 100644 --- a/src/main/resources/bidder-config/axonix.yaml +++ b/src/main/resources/bidder-config/axonix.yaml @@ -13,3 +13,9 @@ adapters: - native supported-vendors: vendor-id: 678 + usersync: + cookie-family-name: axonix + redirect: + support-cors: false + url: https://openrtb-us-east-1.axonix.com/syn?redirect={{redirect_url}} + uid-macro: 'xxEMODO_IDxx' diff --git a/src/main/resources/bidder-config/bidmatic.yaml b/src/main/resources/bidder-config/bidmatic.yaml new file mode 100644 index 00000000000..47636ad0361 --- /dev/null +++ b/src/main/resources/bidder-config/bidmatic.yaml @@ -0,0 +1,13 @@ +adapters: + bidmatic: + endpoint: http://adapter.bidmatic.io/pbs/ortb + meta-info: + maintainer-email: advertising@bidmatic.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 1134 diff --git a/src/main/resources/bidder-config/bigoad.yaml b/src/main/resources/bidder-config/bigoad.yaml new file mode 100644 index 00000000000..becccce1ff5 --- /dev/null +++ b/src/main/resources/bidder-config/bigoad.yaml @@ -0,0 +1,26 @@ +adapters: + bigoad: + endpoint: https://api.imotech.tech/Ad/GetAdOut?sspid={{SspId}} + geoscope: + - USA + - RUS + - JPN + - BRA + - KOR + - IDN + - TUR + - SAU + - MEX + endpoint-compression: gzip + meta-info: + maintainer-email: bigoads-prebid@bigo.sg + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/bizzclick.yaml b/src/main/resources/bidder-config/bizzclick.yaml deleted file mode 100644 index afb208477ae..00000000000 --- a/src/main/resources/bidder-config/bizzclick.yaml +++ /dev/null @@ -1,15 +0,0 @@ -adapters: - bizzclick: - endpoint: http://us-e-node1.bizzclick.com/bid?rtb_seat_id={{.SourceId}}&secret_key={{.AccountID}} - meta-info: - maintainer-email: support@bizzclick.com - app-media-types: - - banner - - video - - native - site-media-types: - - banner - - video - - native - supported-vendors: - vendor-id: 0 diff --git a/src/main/resources/bidder-config/blasto.yaml b/src/main/resources/bidder-config/blasto.yaml new file mode 100644 index 00000000000..d202f0acb2b --- /dev/null +++ b/src/main/resources/bidder-config/blasto.yaml @@ -0,0 +1,22 @@ +# Contact support@blasto.ai to connect with Blasto exchange. +# We have the following regional endpoint sub-domains: +# US East: t-us +# EU: t-eu +# APAC: t-apac +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +adapters: + blasto: + endpoint: http://t-us.blasto.ai/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} + endpoint-compression: gzip + meta-info: + maintainer-email: support@blasto.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/bluesea.yaml b/src/main/resources/bidder-config/bluesea.yaml index 91626852f12..23f6a7a702a 100644 --- a/src/main/resources/bidder-config/bluesea.yaml +++ b/src/main/resources/bidder-config/bluesea.yaml @@ -10,5 +10,8 @@ adapters: - video - native site-media-types: + - banner + - video + - native supported-vendors: - vendor-id: 0 + vendor-id: 1294 diff --git a/src/main/resources/bidder-config/boldwin.yaml b/src/main/resources/bidder-config/boldwin.yaml index 8a488a39807..3b0855882cf 100644 --- a/src/main/resources/bidder-config/boldwin.yaml +++ b/src/main/resources/bidder-config/boldwin.yaml @@ -2,7 +2,7 @@ adapters: boldwin: endpoint: http://ssp.videowalldirect.com/pserver meta-info: - maintainer-email: wls_demo_box@smartyads.com + maintainer-email: info@bold-win.com app-media-types: - banner - video diff --git a/src/main/resources/bidder-config/bwx.yaml b/src/main/resources/bidder-config/bwx.yaml new file mode 100644 index 00000000000..ef5a82eece0 --- /dev/null +++ b/src/main/resources/bidder-config/bwx.yaml @@ -0,0 +1,15 @@ +adapters: + bwx: + endpoint: http://rtb.boldwin.live?pid={{SourceId}}&host={{Host}}&pbs=1 + meta-info: + maintainer-email: prebid@bold-win.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/ccx.yaml b/src/main/resources/bidder-config/ccx.yaml deleted file mode 100644 index 5f4bba80bca..00000000000 --- a/src/main/resources/bidder-config/ccx.yaml +++ /dev/null @@ -1,15 +0,0 @@ -adapters: - ccx: - endpoint: https://delivery.clickonometrics.pl/ortb/prebid/bid - meta-info: - maintainer-email: it@clickonometrics.pl - site-media-types: - - banner - - video - vendor-id: 773 - usersync: - cookie-family-name: ccx - redirect: - url: https://sync.clickonometrics.pl/prebid/set-cookie?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb={{redirect_url}} - support-cors: false - uid-macro: '${USER_ID}' diff --git a/src/main/resources/bidder-config/cointraffic.yaml b/src/main/resources/bidder-config/cointraffic.yaml new file mode 100644 index 00000000000..e105320ad05 --- /dev/null +++ b/src/main/resources/bidder-config/cointraffic.yaml @@ -0,0 +1,11 @@ +adapters: + cointraffic: + endpoint: https://apps.adsgravity.io/pbs/v1/request + meta-info: + maintainer-email: tech@cointraffic.io + app-media-types: + - banner + site-media-types: + - banner + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/concert.yaml b/src/main/resources/bidder-config/concert.yaml new file mode 100644 index 00000000000..51e0d587cd4 --- /dev/null +++ b/src/main/resources/bidder-config/concert.yaml @@ -0,0 +1,16 @@ +adapters: + concert: + endpoint: https://bids.concert.io/bids/openrtb + endpoint-compression: gzip + meta-info: + maintainer-email: support@concert.io + app-media-types: + - banner + - video + - audio + site-media-types: + - banner + - video + - audio + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/connectad.yaml b/src/main/resources/bidder-config/connectad.yaml index 48b448a0b30..e51102b891c 100644 --- a/src/main/resources/bidder-config/connectad.yaml +++ b/src/main/resources/bidder-config/connectad.yaml @@ -1,6 +1,13 @@ adapters: connectad: - endpoint: http://bidder.connectad.io/API?src=pbs + # Please uncomment the appropriate endpoint URL for your datacenter + # Europe + endpoint: "http://bidder.connectad.io/API?src=pbs" + # North/South America + # endpoint: "http://bidder-us.connectad.io/API?src=pbs" + # APAC + # endpoint: "http://bidder-apac.connectad.io/API?src=pbs" + endpoint-compression: gzip meta-info: maintainer-email: support@connectad.io app-media-types: @@ -11,6 +18,9 @@ adapters: vendor-id: 138 usersync: cookie-family-name: connectad + redirect: + url: https://sync.connectad.io/ImageSyncer?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&cb={{redirect_url}} + support-cors: false iframe: - url: https://cdn.connectad.io/connectmyusers.php?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb={{redirect_url}} + url: https://sync.connectad.io/iFrameSyncer?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&cb={{redirect_url}} support-cors: false diff --git a/src/main/resources/bidder-config/consumable.yaml b/src/main/resources/bidder-config/consumable.yaml index 7bd28634f7d..daf074a822f 100644 --- a/src/main/resources/bidder-config/consumable.yaml +++ b/src/main/resources/bidder-config/consumable.yaml @@ -2,11 +2,15 @@ adapters: consumable: endpoint: https://e.serverbid.com/api/v2 meta-info: - maintainer-email: naffis@consumable.com + maintainer-email: prebid@consumable.com app-media-types: - banner + - audio + - video site-media-types: - banner + - audio + - video supported-vendors: vendor-id: 591 usersync: diff --git a/src/main/resources/bidder-config/copper6ssp.yaml b/src/main/resources/bidder-config/copper6ssp.yaml new file mode 100644 index 00000000000..bc7ceceb4b4 --- /dev/null +++ b/src/main/resources/bidder-config/copper6ssp.yaml @@ -0,0 +1,25 @@ +adapters: + copper6ssp: + endpoint: https://endpoint.copper6.com/ + meta-info: + maintainer-email: info@copper6.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: copper6ssp + redirect: + support-cors: false + url: https://csync.copper6.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' + iframe: + support-cors: false + url: https://csync.copper6.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/cpmstar.yaml b/src/main/resources/bidder-config/cpmstar.yaml index d85bf27a51c..a6745317f53 100644 --- a/src/main/resources/bidder-config/cpmstar.yaml +++ b/src/main/resources/bidder-config/cpmstar.yaml @@ -10,9 +10,13 @@ adapters: - banner - video supported-vendors: - vendor-id: 0 + vendor-id: 1317 usersync: cookie-family-name: cpmstar + iframe: + url: https://server.cpmstar.com/usersync.aspx?ifr=1&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' redirect: url: https://server.cpmstar.com/usersync.aspx?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false diff --git a/src/main/resources/bidder-config/criteo.yaml b/src/main/resources/bidder-config/criteo.yaml index cf10a5cf480..a863ba2beb8 100644 --- a/src/main/resources/bidder-config/criteo.yaml +++ b/src/main/resources/bidder-config/criteo.yaml @@ -6,18 +6,20 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 91 usersync: cookie-family-name: criteo redirect: - url: https://ssp-sync.criteo.com/user-sync/redirect?gdprapplies={{gdpr}}&gdpr={{gdpr_consent}}&ccpa={{us_privacy}}&redir={{redirect_url}}&profile=230 + url: https://ssp-sync.criteo.com/user-sync/redirect?gdprapplies={{gdpr}}&gdpr={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}}&profile=230 support-cors: false uid-macro: '${CRITEO_USER_ID}' iframe: - url: https://ssp-sync.criteo.com/user-sync/iframe?gdprapplies={{gdpr}}&gdpr={{gdpr_consent}}&ccpa={{us_privacy}}&redir={{redirect_url}}&profile=230 + url: https://ssp-sync.criteo.com/user-sync/iframe?gdprapplies={{gdpr}}&gdpr={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}}&profile=230 support-cors: false uid-macro: '${CRITEO_USER_ID}' diff --git a/src/main/resources/bidder-config/definemedia.yaml b/src/main/resources/bidder-config/definemedia.yaml new file mode 100644 index 00000000000..ece3f8dc785 --- /dev/null +++ b/src/main/resources/bidder-config/definemedia.yaml @@ -0,0 +1,11 @@ +adapters: + definemedia: + endpoint: https://rtb.conative.network/openrtb2/auction + meta-info: + maintainer-email: development@definemedia.de + app-media-types: + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 440 diff --git a/src/main/resources/bidder-config/displayio.yaml b/src/main/resources/bidder-config/displayio.yaml new file mode 100644 index 00000000000..7c3b5f52ef0 --- /dev/null +++ b/src/main/resources/bidder-config/displayio.yaml @@ -0,0 +1,17 @@ +adapters: + displayio: + endpoint: https://prebid.display.io/?publisher={{PublisherID}} + endpoint-compression: gzip + modifying-vast-xml-allowed: true + geoscope: + - global + meta-info: + maintainer-email: contact@display.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/driftpixel.yaml b/src/main/resources/bidder-config/driftpixel.yaml new file mode 100644 index 00000000000..384d75db00c --- /dev/null +++ b/src/main/resources/bidder-config/driftpixel.yaml @@ -0,0 +1,15 @@ +adapters: + driftpixel: + endpoint: http://rtb.driftpixel.live?pid={{SourceId}}&host={{Host}}&pbs=1 + meta-info: + maintainer-email: developer@driftpixel.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/emxdigital.yaml b/src/main/resources/bidder-config/emxdigital.yaml index 5862a06f4ba..34080708c3e 100644 --- a/src/main/resources/bidder-config/emxdigital.yaml +++ b/src/main/resources/bidder-config/emxdigital.yaml @@ -4,6 +4,10 @@ adapters: aliases: cadent_aperture_mx: enabled: false + # CadentAperture only operates in North America + geoscope: + - USA + - CAN meta-info: maintainer-email: contactaperturemx@cadent.tv app-media-types: diff --git a/src/main/resources/bidder-config/epsilon.yaml b/src/main/resources/bidder-config/epsilon.yaml index 8fea3cc382b..27d1ed14343 100644 --- a/src/main/resources/bidder-config/epsilon.yaml +++ b/src/main/resources/bidder-config/epsilon.yaml @@ -1,6 +1,7 @@ adapters: epsilon: - endpoint: http://api.hb.ad.cpe.dotomi.com/s2s/header/24 + endpoint: http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb + ortb-version: "2.6" aliases: conversant: usersync: diff --git a/src/main/resources/bidder-config/escalax.yaml b/src/main/resources/bidder-config/escalax.yaml new file mode 100644 index 00000000000..8c6c44dbdea --- /dev/null +++ b/src/main/resources/bidder-config/escalax.yaml @@ -0,0 +1,17 @@ +adapters: + escalax: + endpoint: http://bidder_us.escalax.io/?partner={{.SourceId}}&token={{.AccountID}}&type=pbs + modifying-vast-xml-allowed: true + endpoint-compression: gzip + meta-info: + maintainer-email: connect@escalax.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/evolution.yaml b/src/main/resources/bidder-config/evolution.yaml index 2101ba5089b..3101dadcfdb 100644 --- a/src/main/resources/bidder-config/evolution.yaml +++ b/src/main/resources/bidder-config/evolution.yaml @@ -19,3 +19,7 @@ adapters: url: https://sync.e-volution.ai/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&redirect={{redirect_url}} support-cors: false uid-macro: '[UID]' + iframe: + url: https://sync.e-volution.ai/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/freewheelssp.yaml b/src/main/resources/bidder-config/freewheelssp.yaml index 1756a47cbd7..b198a837a9c 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -1,7 +1,8 @@ adapters: freewheelssp: endpoint: https://ads.stickyadstv.com/openrtb/dsp - modifyingVastXmlAllowed: true + ortb-version: "2.6" + modifying-vast-xml-allowed: true meta-info: maintainer-email: prebid-maintainer@freewheel.com app-media-types: diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index 01f7c3fa45f..06ec164dfd9 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -4,6 +4,64 @@ adapters: aliases: genericAlias: enabled: false + adrino: + enabled: false + endpoint: https://prd-prebid-bidder.adrino.io/openrtb/bid + meta-info: + maintainer-email: dev@adrino.pl + app-media-types: + site-media-types: + - native + supported-vendors: + vendor-id: 1072 + ccx: + enabled: false + endpoint: https://delivery.clickonometrics.pl/ortb/prebid/bid + meta-info: + maintainer-email: it@clickonometrics.pl + site-media-types: + - banner + - video + vendor-id: 773 + usersync: + enabled: true + cookie-family-name: ccx + redirect: + url: https://sync.clickonometrics.pl/prebid/set-cookie?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb={{redirect_url}} + support-cors: false + uid-macro: '${USER_ID}' + infytv: + enabled: false + endpoint: https://nxs.infy.tv/pbs/openrtb + meta-info: + maintainer-email: tech+hb@infy.tv + app-media-types: + - video + site-media-types: + - video + supported-vendors: + vendor-id: 0 + zeta_global_ssp: + enabled: false + endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA + endpoint-compression: gzip + meta-info: + maintainer-email: DL-Zeta-SSP@zetaglobal.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 833 + usersync: + enabled: true + cookie-family-name: zeta_global_ssp + redirect: + url: https://ssp.disqus.com/redirectuser?sid=GET_SID_FROM_ZETA&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} + uid-macro: 'BUYERUID' + support-cors: false blue: enabled: false endpoint: https://prebid-us-east-1.getblue.io/?src=prebid @@ -56,15 +114,15 @@ adapters: - video - native site-media-types: - - banner - - video - - native + - banner + - video + - native supported-vendors: vendor-id: 263 usersync: cookie-family-name: nativo redirect: - url: http://jadserve.postrelease.com/suid/101787?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&ntv_gpp_consent={{gpp}}&ntv_r={{redirect_url}} + url: https://jadserve.postrelease.com/suid/101787?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&ntv_gpp_consent={{gpp}}&ntv_r={{redirect_url}} support-cors: false uid-macro: 'NTV_USER_ID' meta-info: diff --git a/src/main/resources/bidder-config/grid.yaml b/src/main/resources/bidder-config/grid.yaml index b9365394326..e735a5159f7 100644 --- a/src/main/resources/bidder-config/grid.yaml +++ b/src/main/resources/bidder-config/grid.yaml @@ -6,14 +6,16 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 686 usersync: cookie-family-name: grid redirect: - url: https://x.bidswitch.net/check_uuid/{{redirect_url}} + url: https://x.bidswitch.net/check_uuid/{{redirect_url}}?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&us_privacy={{us_privacy}} support-cors: false uid-macro: '${BSW_UUID}' diff --git a/src/main/resources/bidder-config/gumgum.yaml b/src/main/resources/bidder-config/gumgum.yaml index e6bd1d4e98e..b3ee922dce4 100644 --- a/src/main/resources/bidder-config/gumgum.yaml +++ b/src/main/resources/bidder-config/gumgum.yaml @@ -1,6 +1,7 @@ adapters: gumgum: endpoint: https://g2.gumgum.com/providers/prbds2s/bid + ortb-version: "2.6" meta-info: maintainer-email: prebid@gumgum.com app-media-types: diff --git a/src/main/resources/bidder-config/infytv.yaml b/src/main/resources/bidder-config/infytv.yaml deleted file mode 100644 index fdf66ff92ac..00000000000 --- a/src/main/resources/bidder-config/infytv.yaml +++ /dev/null @@ -1,11 +0,0 @@ -adapters: - infytv: - endpoint: https://nxs.infy.tv/pbs/openrtb - meta-info: - maintainer-email: tech+hb@infy.tv - app-media-types: - - video - site-media-types: - - video - supported-vendors: - vendor-id: 0 diff --git a/src/main/resources/bidder-config/inmobi.yaml b/src/main/resources/bidder-config/inmobi.yaml index bea26c19997..2107d52524a 100644 --- a/src/main/resources/bidder-config/inmobi.yaml +++ b/src/main/resources/bidder-config/inmobi.yaml @@ -10,11 +10,12 @@ adapters: site-media-types: - banner - video + - native supported-vendors: vendor-id: 333 usersync: cookie-family-name: inmobi - redirect: - url: https://sync.inmobi.com/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + iframe: + url: https://sync.inmobi.com/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false uid-macro: '{ID5UID}' diff --git a/src/main/resources/bidder-config/ix.yaml b/src/main/resources/bidder-config/ix.yaml index 0a5c8819df9..1a5fa58aa44 100644 --- a/src/main/resources/bidder-config/ix.yaml +++ b/src/main/resources/bidder-config/ix.yaml @@ -1,6 +1,7 @@ adapters: ix: endpoint: https:// + ortb-version: "2.6" meta-info: maintainer-email: pdu-supply-prebid@indexexchange.com app-media-types: diff --git a/src/main/resources/bidder-config/kargo.yaml b/src/main/resources/bidder-config/kargo.yaml index 79532582447..56d5e9ea22d 100644 --- a/src/main/resources/bidder-config/kargo.yaml +++ b/src/main/resources/bidder-config/kargo.yaml @@ -1,8 +1,9 @@ adapters: kargo: endpoint: https://krk.kargo.com/api/v1/openrtb + ortb-version: "2.6" endpoint-compression: gzip - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: kraken@kargo.com app-media-types: @@ -15,6 +16,6 @@ adapters: usersync: cookie-family-name: kargo redirect: - url: https://crb.kargo.com/api/v1/dsync/PrebidServer?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} + url: https://crb.kargo.com/api/v1/dsync/PrebidServer?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/lemmadigital.yaml b/src/main/resources/bidder-config/lemmadigital.yaml index 1d7208aff4f..3f71b2d016c 100644 --- a/src/main/resources/bidder-config/lemmadigital.yaml +++ b/src/main/resources/bidder-config/lemmadigital.yaml @@ -1,6 +1,6 @@ adapters: lemmadigital: - endpoint: https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{PublisherID}}&aid={{AdUnit}} + endpoint: https://pbid.lemmamedia.com/lemma/servad?src=prebid&pid={{PublisherID}}&aid={{AdUnit}} meta-info: maintainer-email: support@lemmatechnologies.com endpoint-compression: gzip @@ -13,3 +13,9 @@ adapters: - video supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: lemmadigital + redirect: + url: https://sync.lemmadigital.com/setuid?publisher=850&redirect={{redirect_url}} + support-cors: false + uid-macro: '${UUID}' diff --git a/src/main/resources/bidder-config/liftoff.yaml b/src/main/resources/bidder-config/liftoff.yaml deleted file mode 100644 index 5b415c8b84a..00000000000 --- a/src/main/resources/bidder-config/liftoff.yaml +++ /dev/null @@ -1,13 +0,0 @@ -adapters: - liftoff: - endpoint: https://rtb.ads.vungle.com/bid/t/c770f32 - modifying-vast-xml-allowed: true - endpoint-compression: gzip - meta-info: - maintainer-email: vxssp@liftoff.io - app-media-types: - - video - site-media-types: - - video - supported-vendors: - vendor-id: 0 diff --git a/src/main/resources/bidder-config/limelightDigital.yaml b/src/main/resources/bidder-config/limelightDigital.yaml index ab2336287e0..48c238c3840 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -2,6 +2,8 @@ adapters: limelightDigital: endpoint: http://ads-pbs.ortb.net/openrtb/{{PublisherID}}?host={{Host}} aliases: + filmzie: + enabled: false iionads: enabled: false endpoint: http://ads-pbs.iionads.com/openrtb/{{PublisherID}}?host={{Host}} @@ -34,6 +36,11 @@ adapters: embimedia: enabled: false endpoint: http://ads-pbs.bidder-embi.media/openrtb/{{PublisherID}}?host={{Host}} + tgm: + enabled: false + streamlyn: + enabled: false + endpoint: http://rtba.bidsxchange.com/openrtb/{{PublisherID}}?host={{Host}} meta-info: maintainer-email: engineering@project-limelight.com app-media-types: diff --git a/src/main/resources/bidder-config/loopme.yaml b/src/main/resources/bidder-config/loopme.yaml index 321c6898762..bee027b1b18 100644 --- a/src/main/resources/bidder-config/loopme.yaml +++ b/src/main/resources/bidder-config/loopme.yaml @@ -1,15 +1,23 @@ adapters: loopme: - endpoint: http://prebid-eu.loopmertb.com + endpoint: http://prebid.loopmertb.com meta-info: - maintainer-email: support@loopme.com + maintainer-email: prebid@loopme.com app-media-types: - banner - video + - audio - native site-media-types: - banner - video + - audio - native supported-vendors: vendor-id: 109 + usersync: + url: https://csync.loopme.me/?pubid=11393&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + redirect-url: /setuid?bidder=loopme&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={udid} + cookie-family-name: loopme + type: redirect + support-cors: false diff --git a/src/main/resources/bidder-config/loyal.yaml b/src/main/resources/bidder-config/loyal.yaml new file mode 100644 index 00000000000..07e67800974 --- /dev/null +++ b/src/main/resources/bidder-config/loyal.yaml @@ -0,0 +1,17 @@ +adapters: + loyal: + endpoint: "https://us-east-1.loyal.app/pserver" + geoscope: + - USA + meta-info: + maintainer-email: "hello@loyal.app" + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: [] + vendor-id: 0 diff --git a/src/main/resources/bidder-config/mabidder.yaml b/src/main/resources/bidder-config/mabidder.yaml index c87a69f4c81..daecc7b2385 100644 --- a/src/main/resources/bidder-config/mabidder.yaml +++ b/src/main/resources/bidder-config/mabidder.yaml @@ -1,6 +1,9 @@ adapters: mabidder: endpoint: https://prebid.ecdrsvc.com/pbs + # This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions: + geoscope: + - "CAN" endpoint-compression: gzip meta-info: maintainer-email: lmprebidadapter@loblaw.ca diff --git a/src/main/resources/bidder-config/mediago.yaml b/src/main/resources/bidder-config/mediago.yaml new file mode 100644 index 00000000000..1fceb2e034f --- /dev/null +++ b/src/main/resources/bidder-config/mediago.yaml @@ -0,0 +1,29 @@ +adapters: + mediago: + endpoint: https://REGION.mediago.io/api/bid?tn={{AccountID}} + endpoint-compression: gzip + geoscope: + - USA + - DEU + - JPN + - GBR + - KOR + - CAN + - FRA + - ITA + meta-info: + maintainer-email: ext_mediago_cm@baidu.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 1020 + usersync: + cookie-family-name: mediago + redirect: + url: https://trace.mediago.io/ju/cs/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/melozen.yaml b/src/main/resources/bidder-config/melozen.yaml new file mode 100644 index 00000000000..8626fd4eabe --- /dev/null +++ b/src/main/resources/bidder-config/melozen.yaml @@ -0,0 +1,19 @@ +adapters: + melozen: + endpoint: https://prebid.melozen.com/rtb/v2/bid?publisher_id={{PublisherID}} + endpoint-compression: gzip + modifying-vast-xml-allowed: true + geoscope: + - global + meta-info: + maintainer-email: DSP@melodong.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/metax.yaml b/src/main/resources/bidder-config/metax.yaml new file mode 100644 index 00000000000..7e87526ac0b --- /dev/null +++ b/src/main/resources/bidder-config/metax.yaml @@ -0,0 +1,18 @@ +# The MetaX Bidding adapter requires setup before beginning. Please contact us at +adapters: + metax: + endpoint: https://hb.metaxads.com/prebid?sid={{publisherId}}&adunit={{adUnit}}&source=prebid-server + meta-info: + maintainer-email: prebid@metaxsoft.com + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 1301 diff --git a/src/main/resources/bidder-config/mgidx.yaml b/src/main/resources/bidder-config/mgidx.yaml index 0228aa09cc7..4989dc43461 100644 --- a/src/main/resources/bidder-config/mgidx.yaml +++ b/src/main/resources/bidder-config/mgidx.yaml @@ -1,6 +1,8 @@ adapters: mgidX: - endpoint: https://us-east-x.mgid.com/pserver + # We have the following regional endpoint domains: 'us-east-x' and 'eu-x' + # Please deploy this config in each of your datacenters with the appropriate regional subdomain + endpoint: https://REGION.mgid.com/pserver meta-info: maintainer-email: prebid@mgid.com app-media-types: diff --git a/src/main/resources/bidder-config/missena.yaml b/src/main/resources/bidder-config/missena.yaml new file mode 100644 index 00000000000..4f1ea9e4f8f --- /dev/null +++ b/src/main/resources/bidder-config/missena.yaml @@ -0,0 +1,18 @@ +adapters: + missena: + endpoint: https://bid.missena.io/ + meta-info: + maintainer-email: prebid@missena.com + modifying-vast-xml-allowed: true + app-media-types: + - banner + site-media-types: + - banner + supported-vendors: + vendor-id: 687 + usersync: + cookie-family-name: missena + iframe: + url: https://sync.missena.io/iframe?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/mobfoxpb.yaml b/src/main/resources/bidder-config/mobfoxpb.yaml index f9d05c7e2c3..bed22ef4094 100644 --- a/src/main/resources/bidder-config/mobfoxpb.yaml +++ b/src/main/resources/bidder-config/mobfoxpb.yaml @@ -2,7 +2,7 @@ adapters: mobfoxpb: endpoint: http://bes.mobfox.com/?c=__route__&m=__method__&key=__key__ meta-info: - maintainer-email: platform@mobfox.com + maintainer-email: support@mobfox.com app-media-types: - banner - video diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml index e16a4ac1210..ea6645feb96 100644 --- a/src/main/resources/bidder-config/mobilefuse.yaml +++ b/src/main/resources/bidder-config/mobilefuse.yaml @@ -1,6 +1,7 @@ adapters: mobilefuse: endpoint: http://mfx.mobilefuse.com/openrtb?pub_id= + ortb-version: "2.6" endpoint-compression: gzip # This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions: geoscope: diff --git a/src/main/resources/bidder-config/nextmillennium.yaml b/src/main/resources/bidder-config/nextmillennium.yaml index 51524341a45..f1039b8e45b 100644 --- a/src/main/resources/bidder-config/nextmillennium.yaml +++ b/src/main/resources/bidder-config/nextmillennium.yaml @@ -5,8 +5,10 @@ adapters: maintainer-email: accountmanagers@nextmillennium.io app-media-types: - banner + - video site-media-types: - banner + - video supported-vendors: vendor-id: 1060 usersync: diff --git a/src/main/resources/bidder-config/nobid.yaml b/src/main/resources/bidder-config/nobid.yaml index dc93b6f6919..2d0aaa34db7 100644 --- a/src/main/resources/bidder-config/nobid.yaml +++ b/src/main/resources/bidder-config/nobid.yaml @@ -17,3 +17,7 @@ adapters: url: https://ads.servenobid.com/getsync?tek=pbs&ver=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false uid-macro: '$UID' + iframe: + url: https://public.servenobid.com/sync.html?tek=pbs&ver=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/onetag.yaml b/src/main/resources/bidder-config/onetag.yaml index d761209a15d..34f2d844158 100644 --- a/src/main/resources/bidder-config/onetag.yaml +++ b/src/main/resources/bidder-config/onetag.yaml @@ -21,3 +21,7 @@ adapters: url: https://onetag-sys.com/usync/?redir={{redirect_url}}&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}} support-cors: false uid-macro: '${USER_TOKEN}' + redirect: + url: https://onetag-sys.com/usync/?tag=img&redir={{redirect_url}}&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}} + support-cors: false + uid-macro: '${USER_TOKEN}' diff --git a/src/main/resources/bidder-config/openweb.yaml b/src/main/resources/bidder-config/openweb.yaml index 70ccffb1e65..1e8bfbea287 100644 --- a/src/main/resources/bidder-config/openweb.yaml +++ b/src/main/resources/bidder-config/openweb.yaml @@ -1,6 +1,6 @@ adapters: openweb: - endpoint: http://ghb.spotim.market/pbs/ortb + endpoint: https://pbs.openwebmp.com/pbs meta-info: maintainer-email: monetization@openweb.com app-media-types: @@ -11,3 +11,9 @@ adapters: - video supported-vendors: vendor-id: 280 + usersync: + cookie-family-name: openweb + iframe: + url: https://pbs-cs.openwebmp.com/pbs-iframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[PBS_UID]' diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml index c6fa7e65410..9e8454131d4 100644 --- a/src/main/resources/bidder-config/openx.yaml +++ b/src/main/resources/bidder-config/openx.yaml @@ -1,6 +1,8 @@ adapters: openx: endpoint: http://rtb.openx.net/prebid + ortb-version: "2.6" + endpoint-compression: gzip meta-info: maintainer-email: prebid@openx.com app-media-types: diff --git a/src/main/resources/bidder-config/oraki.yaml b/src/main/resources/bidder-config/oraki.yaml new file mode 100644 index 00000000000..f5197ac83ff --- /dev/null +++ b/src/main/resources/bidder-config/oraki.yaml @@ -0,0 +1,21 @@ +adapters: + oraki: + endpoint: https://eu1.oraki.io/pserver + meta-info: + maintainer-email: prebid@oraki.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: oraki + redirect: + support-cors: false + url: https://sync.oraki.io/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/ownadx.yaml b/src/main/resources/bidder-config/ownadx.yaml new file mode 100644 index 00000000000..c836d847f98 --- /dev/null +++ b/src/main/resources/bidder-config/ownadx.yaml @@ -0,0 +1,20 @@ +adapters: + ownadx: + endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{SeatID}}/{{SspID}}?token={{TokenID}}" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid-team@techbravo.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: ownadx + redirect: + url: https://sync.spoutroserve.com/user-sync?t=image&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&s3={{redirect_url}} + support-cors: false + uid-macro: '{USER_ID}' diff --git a/src/main/resources/bidder-config/pgamssp.yaml b/src/main/resources/bidder-config/pgamssp.yaml index e95261073f3..e2088e63e0e 100644 --- a/src/main/resources/bidder-config/pgamssp.yaml +++ b/src/main/resources/bidder-config/pgamssp.yaml @@ -16,6 +16,6 @@ adapters: usersync: cookie-family-name: pgamssp redirect: - url: https://cs.pgammedia.com/pserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} + url: https://cs.pgammedia.com/pserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/playdigo.yaml b/src/main/resources/bidder-config/playdigo.yaml new file mode 100644 index 00000000000..adb14424c1b --- /dev/null +++ b/src/main/resources/bidder-config/playdigo.yaml @@ -0,0 +1,27 @@ +adapters: + playdigo: + endpoint: https://server.playdigo.com/pserver + geoscope: + - USA + meta-info: + maintainer-email: yr@playdigo.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1302 + usersync: + cookie-family-name: playdigo + redirect: + url: https://cs.playdigo.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + iframe: + url: https://cs.playdigo.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/pubrise.yaml b/src/main/resources/bidder-config/pubrise.yaml new file mode 100644 index 00000000000..ae7768d44b2 --- /dev/null +++ b/src/main/resources/bidder-config/pubrise.yaml @@ -0,0 +1,25 @@ +adapters: + pubrise: + endpoint: https://backend.pubrise.ai/ + meta-info: + maintainer-email: prebid@pubrise.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: pubrise + redirect: + support-cors: false + url: https://sync.pubrise.ai/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' + iframe: + support-cors: false + url: https://sync.pubrise.ai/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/pulsepoint.yaml b/src/main/resources/bidder-config/pulsepoint.yaml index 3e1d27ee281..8cd407e3b74 100644 --- a/src/main/resources/bidder-config/pulsepoint.yaml +++ b/src/main/resources/bidder-config/pulsepoint.yaml @@ -1,6 +1,7 @@ adapters: pulsepoint: endpoint: http://bid.contextweb.com/header/s/ortb/prebid-s2s + ortb-version: "2.6" meta-info: maintainer-email: ExchangeTeam@pulsepoint.com app-media-types: diff --git a/src/main/resources/bidder-config/qt.yaml b/src/main/resources/bidder-config/qt.yaml new file mode 100644 index 00000000000..8c81123c8ca --- /dev/null +++ b/src/main/resources/bidder-config/qt.yaml @@ -0,0 +1,21 @@ +adapters: + qt: + endpoint: https://endpoint1.qt.io/pserver + meta-info: + maintainer-email: qtssp-support@qt.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1331 + usersync: + cookie-family-name: qt + redirect: + support-cors: false + url: https://cs.qt.io/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/readpeak.yaml b/src/main/resources/bidder-config/readpeak.yaml new file mode 100644 index 00000000000..0982f1f9d17 --- /dev/null +++ b/src/main/resources/bidder-config/readpeak.yaml @@ -0,0 +1,15 @@ +adapters: + readpeak: + endpoint: https://dsp.readpeak.com/header/prebid + geoscope: + - EEA + meta-info: + maintainer-email: devteam@readpeak.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 290 diff --git a/src/main/resources/bidder-config/rise.yaml b/src/main/resources/bidder-config/rise.yaml index f85876dd998..12fc11eb92e 100644 --- a/src/main/resources/bidder-config/rise.yaml +++ b/src/main/resources/bidder-config/rise.yaml @@ -15,6 +15,6 @@ adapters: usersync: cookie-family-name: rise iframe: - url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false uid-macro: '[PBS_UID]' diff --git a/src/main/resources/bidder-config/roulax.yaml b/src/main/resources/bidder-config/roulax.yaml new file mode 100644 index 00000000000..b9055e5df4a --- /dev/null +++ b/src/main/resources/bidder-config/roulax.yaml @@ -0,0 +1,15 @@ +adapters: + roulax: + endpoint: http://dsp.rcoreads.com/api/{{PublisherID}}?pid={{AccountID}} + meta-info: + maintainer-email: bussiness@roulax.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/rtbhouse.yaml b/src/main/resources/bidder-config/rtbhouse.yaml index fd2f73da770..a428897379c 100644 --- a/src/main/resources/bidder-config/rtbhouse.yaml +++ b/src/main/resources/bidder-config/rtbhouse.yaml @@ -1,15 +1,28 @@ adapters: rtbhouse: - endpoint: https://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids + # Contact prebid@rtbhouse.com to ask about enabling a connection to the bidder. + # Please configure the following endpoints for your datacenter + # EMEA + endpoint: http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids + # US East + # endpoint: http://prebidserver-s2s-ash.creativecdn.com/bidder/prebidserver/bids + # US West + # endpoint: http://prebidserver-s2s-phx.creativecdn.com/bidder/prebidserver/bids + # APAC + # endpoint: http://prebidserver-s2s-sin.creativecdn.com/bidder/prebidserver/bids + geoscope: + - global endpoint-compression: gzip meta-info: maintainer-email: prebid@rtbhouse.com app-media-types: - banner - native + - video site-media-types: - banner - native + - video supported-vendors: vendor-id: 16 usersync: diff --git a/src/main/resources/bidder-config/rubicon.yaml b/src/main/resources/bidder-config/rubicon.yaml index a7a4fcc1eea..882732f83a5 100644 --- a/src/main/resources/bidder-config/rubicon.yaml +++ b/src/main/resources/bidder-config/rubicon.yaml @@ -11,6 +11,8 @@ adapters: aliases: magnite: enabled: false + ortb: + multiformat-supported: true meta-info: maintainer-email: header-bidding@rubiconproject.com app-media-types: diff --git a/src/main/resources/bidder-config/sharethrough.yaml b/src/main/resources/bidder-config/sharethrough.yaml index d108840dd26..36cc0f0127f 100644 --- a/src/main/resources/bidder-config/sharethrough.yaml +++ b/src/main/resources/bidder-config/sharethrough.yaml @@ -16,6 +16,6 @@ adapters: usersync: cookie-family-name: sharethrough redirect: - url: https://match.sharethrough.com/FGMrCMMc/v1?redirectUri={{redirect_url}} + url: https://match.sharethrough.com/FGMrCMMc/v1?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirectUri={{redirect_url}} support-cors: false uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/smaato.yaml b/src/main/resources/bidder-config/smaato.yaml index 58d5ddd4a17..be20848b4e5 100644 --- a/src/main/resources/bidder-config/smaato.yaml +++ b/src/main/resources/bidder-config/smaato.yaml @@ -7,9 +7,11 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 82 # This bidder does not sync when GDPR is in-scope. Please consider removing the usersync diff --git a/src/main/resources/bidder-config/smarthub.yaml b/src/main/resources/bidder-config/smarthub.yaml index 8b1d8de2869..ebe51135d06 100644 --- a/src/main/resources/bidder-config/smarthub.yaml +++ b/src/main/resources/bidder-config/smarthub.yaml @@ -1,6 +1,22 @@ adapters: smarthub: - endpoint: http://{{Host}}-prebid.smart-hub.io/?seat={{AccountId}}&token={{SourceId}} + endpoint: http://prebid.smart-hub.io/pbserver?partnerName={{Host}}&seat={{AccountID}}&token={{SourceId}} + aliases: + markapp: + enabled: false + endpoint: http://markapp-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + jdpmedia: + enabled: false + endpoint: http://jdpmedia-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + tredio: + enabled: false + endpoint: http://tredio-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + vimayx: + enabled: false + endpoint: http://vimayx-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + felixads: + enabled: false + endpoint: http://felixads-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} meta-info: maintainer-email: support@smart-hub.io app-media-types: diff --git a/src/main/resources/bidder-config/smartx.yaml b/src/main/resources/bidder-config/smartx.yaml index 50a731d8c9d..d0f4498a32a 100644 --- a/src/main/resources/bidder-config/smartx.yaml +++ b/src/main/resources/bidder-config/smartx.yaml @@ -1,6 +1,7 @@ adapters: smartx: endpoint: https://bid.smartclip.net/bid/1005 + ortb-version: "2.6" meta-info: maintainer-email: bidding@smartclip.tv app-media-types: diff --git a/src/main/resources/bidder-config/smrtconnect.yaml b/src/main/resources/bidder-config/smrtconnect.yaml new file mode 100644 index 00000000000..c9ee3a640ef --- /dev/null +++ b/src/main/resources/bidder-config/smrtconnect.yaml @@ -0,0 +1,21 @@ +adapters: + smrtconnect: + endpoint: https://amp.smrtconnect.com/openrtb2/auction?supply_id={{SupplyId}} + # This bidder does not operate globally. Please consider setting "disabled: true" in European datacenters. + geoscope: + - "!EEA" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid@smrtconnect.com + app-media-types: + - banner + - native + - video + - audio + site-media-types: + - banner + - native + - video + - audio + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/sonobi.yaml b/src/main/resources/bidder-config/sonobi.yaml index a093b8dcd45..c6405555f1a 100644 --- a/src/main/resources/bidder-config/sonobi.yaml +++ b/src/main/resources/bidder-config/sonobi.yaml @@ -6,14 +6,20 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 104 usersync: cookie-family-name: sonobi + iframe: + url: https://sync.go.sonobi.com/uc.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&loc={{redirect_url}} + support-cors: false + uid-macro: '[UID]' redirect: - url: https://sync.go.sonobi.com/us.gif?loc={{redirect_url}} + url: https://sync.go.sonobi.com/us.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&loc={{redirect_url}} support-cors: false uid-macro: '[UID]' diff --git a/src/main/resources/bidder-config/sovrn.yaml b/src/main/resources/bidder-config/sovrn.yaml index d2d22c55c21..8f74ae266b6 100644 --- a/src/main/resources/bidder-config/sovrn.yaml +++ b/src/main/resources/bidder-config/sovrn.yaml @@ -1,7 +1,8 @@ adapters: sovrn: endpoint: http://ap.lijit.com/rtb/bid?src=prebid_server - modifyingVastXmlAllowed: true + endpoint-compression: gzip + modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com app-media-types: diff --git a/src/main/resources/bidder-config/sovrnXsp.yaml b/src/main/resources/bidder-config/sovrnXsp.yaml index 706a06caafe..6a2a626e66f 100644 --- a/src/main/resources/bidder-config/sovrnXsp.yaml +++ b/src/main/resources/bidder-config/sovrnXsp.yaml @@ -2,7 +2,7 @@ adapters: sovrnXsp: endpoint: http://xsp.lijit.com/json/rtb/prebid/server endpoint-compression: gzip - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com app-media-types: diff --git a/src/main/resources/bidder-config/taboola.yaml b/src/main/resources/bidder-config/taboola.yaml index f7401931c3b..f5cc2bfeb62 100644 --- a/src/main/resources/bidder-config/taboola.yaml +++ b/src/main/resources/bidder-config/taboola.yaml @@ -15,11 +15,11 @@ adapters: userSync: cookie-family-name: taboola redirect: - url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false uid-macro: '' iframe: - url: https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false uid-macro: '' diff --git a/src/main/resources/bidder-config/theadx.yaml b/src/main/resources/bidder-config/theadx.yaml new file mode 100644 index 00000000000..b9440fa5776 --- /dev/null +++ b/src/main/resources/bidder-config/theadx.yaml @@ -0,0 +1,22 @@ +adapters: + theadx: + endpoint: https://ssp.theadx.com/request?pbs=1 + meta-info: + maintainer-email: ssp@theadx.com + app-media-types: + - banner + - video + - audio + - native + site-media-types: + - banner + - video + - audio + - native + vendor-id: 556 + usersync: + cookie-family-name: theadx + redirect: + url: https://ssp.theadx.com/cookie?redirect_url={{redirect_url}}&?pbs=1 + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/thetradedesk.yaml b/src/main/resources/bidder-config/thetradedesk.yaml new file mode 100644 index 00000000000..b71a0e38cc2 --- /dev/null +++ b/src/main/resources/bidder-config/thetradedesk.yaml @@ -0,0 +1,15 @@ +adapters: + thetradedesk: + endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}} + meta-info: + maintainer-email: Prebid-Maintainers@thetradedesk.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 21 diff --git a/src/main/resources/bidder-config/tradplus.yaml b/src/main/resources/bidder-config/tradplus.yaml new file mode 100644 index 00000000000..9644f025c45 --- /dev/null +++ b/src/main/resources/bidder-config/tradplus.yaml @@ -0,0 +1,11 @@ +adapters: + tradplus: + endpoint: "https://{{ZoneID}}adx.tradplusad.com/{{AccountID}}/pserver" + meta-info: + maintainer-email: "tpxcontact@tradplus.com" + app-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/trafficgate.yaml b/src/main/resources/bidder-config/trafficgate.yaml index e4dd6b1fcd6..135d61e2fbe 100644 --- a/src/main/resources/bidder-config/trafficgate.yaml +++ b/src/main/resources/bidder-config/trafficgate.yaml @@ -1,7 +1,7 @@ adapters: trafficgate: endpoint: http://{{subdomain}}.bc-plugin.com/?c=o&m=rtb - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: "support@bidscube.com" app-media-types: diff --git a/src/main/resources/bidder-config/triplelift.yaml b/src/main/resources/bidder-config/triplelift.yaml index e8c35e3eb2c..446825e33dc 100644 --- a/src/main/resources/bidder-config/triplelift.yaml +++ b/src/main/resources/bidder-config/triplelift.yaml @@ -1,6 +1,7 @@ adapters: triplelift: endpoint: https://tlx.3lift.com/s2s/auction?sra=1&supplier_id=20 + ortb-version: "2.6" endpoint-compression: gzip meta-info: maintainer-email: prebid@triplelift.com diff --git a/src/main/resources/bidder-config/tripleliftnative.yaml b/src/main/resources/bidder-config/tripleliftnative.yaml index 122039f58f5..e6c1f106f62 100644 --- a/src/main/resources/bidder-config/tripleliftnative.yaml +++ b/src/main/resources/bidder-config/tripleliftnative.yaml @@ -1,6 +1,7 @@ adapters: triplelift_native: - endpoint: http:// + endpoint: https://tlx.3lift.com/s2sn/auction?supplier_id=20 + ortb-version: "2.6" meta-info: maintainer-email: prebid@triplelift.com app-media-types: diff --git a/src/main/resources/bidder-config/trustedstack.yaml b/src/main/resources/bidder-config/trustedstack.yaml new file mode 100644 index 00000000000..466d145e6df --- /dev/null +++ b/src/main/resources/bidder-config/trustedstack.yaml @@ -0,0 +1,23 @@ +adapters: + trustedstack: + endpoint: https://prebid-adapter.trustedstack.com/rtb/pb/trustedstacks2s?src={{PREBID_SERVER_ENDPOINT}} + ortb-version: "2.6" + endpoint-compression: gzip + meta-info: + maintainer-email: product@trustedstack.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1288 + usersync: + cookie-family-name: trustedstack + redirect: + url: https://hb.trustedstack.com/cksync?cs=1&type=pbs&ovsid=setstatuscode&bidder=trustedstack&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '' diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml index 050c61c62d9..af4f8690282 100644 --- a/src/main/resources/bidder-config/unruly.yaml +++ b/src/main/resources/bidder-config/unruly.yaml @@ -1,6 +1,8 @@ adapters: unruly: endpoint: https://targeting.unrulymedia.com/unruly_prebid_server + ortb-version: "2.6" + endpoint-compression: gzip meta-info: maintainer-email: prebidsupport@unrulygroup.com app-media-types: diff --git a/src/main/resources/bidder-config/vidazoo.yaml b/src/main/resources/bidder-config/vidazoo.yaml new file mode 100644 index 00000000000..f99cbb0ee1e --- /dev/null +++ b/src/main/resources/bidder-config/vidazoo.yaml @@ -0,0 +1,20 @@ +adapters: + vidazoo: + endpoint: https://prebidsrvr.cootlogix.com/openrtb/ + endpoint-compression: gzip + meta-info: + maintainer-email: dev@vidazoo.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 744 + usersync: + cookie-family-name: vidazoo + iframe: + url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} + support-cors: false + uid-macro: '${userId}' diff --git a/src/main/resources/bidder-config/vungle.yaml b/src/main/resources/bidder-config/vungle.yaml new file mode 100644 index 00000000000..0a9baf58403 --- /dev/null +++ b/src/main/resources/bidder-config/vungle.yaml @@ -0,0 +1,16 @@ +adapters: + vungle: + endpoint: https://rtb.ads.vungle.com/bid/t/c770f32 + aliases: + liftoff: + enabled: false + modifying-vast-xml-allowed: true + endpoint-compression: gzip + meta-info: + maintainer-email: vxssp@liftoff.io + app-media-types: + - video + site-media-types: + - video + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/yeahmobi.yaml b/src/main/resources/bidder-config/yeahmobi.yaml index a36bc7406c4..082c74a1ecf 100644 --- a/src/main/resources/bidder-config/yeahmobi.yaml +++ b/src/main/resources/bidder-config/yeahmobi.yaml @@ -7,9 +7,5 @@ adapters: - banner - video - native - site-media-types: - - banner - - video - - native supported-vendors: vendor-id: 0 diff --git a/src/main/resources/bidder-config/yieldmo.yaml b/src/main/resources/bidder-config/yieldmo.yaml index 384804a3722..84415ead13e 100644 --- a/src/main/resources/bidder-config/yieldmo.yaml +++ b/src/main/resources/bidder-config/yieldmo.yaml @@ -1,6 +1,7 @@ adapters: yieldmo: endpoint: https://ads.yieldmo.com/exchange/prebid-server + ortb-version: "2.6" meta-info: maintainer-email: prebid@yieldmo.com app-media-types: diff --git a/src/main/resources/bidder-config/zeta_global_ssp.yaml b/src/main/resources/bidder-config/zeta_global_ssp.yaml deleted file mode 100644 index 36926fb335b..00000000000 --- a/src/main/resources/bidder-config/zeta_global_ssp.yaml +++ /dev/null @@ -1,20 +0,0 @@ -adapters: - zeta-global-ssp: - endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA - endpoint-compression: gzip - meta-info: - maintainer-email: DL-Zeta-SSP@zetaglobal.com - app-media-types: - - banner - - video - site-media-types: - - banner - - video - supported-vendors: - vendor-id: 833 - usersync: - cookie-family-name: zeta_global_ssp - redirect: - url: https://ssp.disqus.com/redirectuser?sid=GET_SID_FROM_ZETA&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} - uid-macro: 'BUYERUID' - support-cors: false diff --git a/src/main/resources/bidder-config/zmaticoo.yaml b/src/main/resources/bidder-config/zmaticoo.yaml new file mode 100644 index 00000000000..718fff17436 --- /dev/null +++ b/src/main/resources/bidder-config/zmaticoo.yaml @@ -0,0 +1,12 @@ +adapters: + zmaticoo: + endpoint: https://bid.zmaticoo.com/prebid/bid + meta-info: + maintainer-email: adam.li@eclicktech.com.cn + app-media-types: + - banner + - video + - native + site-media-types: + supported-vendors: + vendor-id: 803 diff --git a/src/main/resources/c3p0.properties b/src/main/resources/c3p0.properties deleted file mode 100644 index 5a823a5deab..00000000000 --- a/src/main/resources/c3p0.properties +++ /dev/null @@ -1,8 +0,0 @@ -# can't turn off retries at all, that's why minimum value is 1 -c3p0.acquireRetryAttempts=1 -# don't wait before retrying -c3p0.acquireRetryDelay=0 -# how long to wait until connection becomes available -c3p0.checkoutTimeout=15000 -# how frequently to test idle pooled connections to avoid seeing broken or stale connections -c3p0.idleConnectionTestPeriod=300 diff --git a/src/main/resources/static/bidder-params/admatic.json b/src/main/resources/static/bidder-params/admatic.json new file mode 100644 index 00000000000..afb6159f184 --- /dev/null +++ b/src/main/resources/static/bidder-params/admatic.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdMatic Adapter Params", + "description": "A schema which validates params accepted by the AdMatic adapter", + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Host Name" + }, + "networkId": { + "type": "integer", + "description": "AdMatic Network Id" + } + }, + "required": [ + "host", + "networkId" + ] +} diff --git a/src/main/resources/static/bidder-params/adtelligent.json b/src/main/resources/static/bidder-params/adtelligent.json index db7931e1ec0..e8dedf33690 100644 --- a/src/main/resources/static/bidder-params/adtelligent.json +++ b/src/main/resources/static/bidder-params/adtelligent.json @@ -2,7 +2,6 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Adtelligent Adapter Params", "description": "A schema which validates params accepted by the Adtelligent adapter", - "type": "object", "properties": { "placementId": { @@ -14,7 +13,10 @@ "description": "An ID which identifies the site selling the impression" }, "aid": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "An ID which identifies the channel" }, "bidFloor": { diff --git a/src/main/resources/static/bidder-params/adtonos.json b/src/main/resources/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..b1ea833f1e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/adtonos.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": [ + "supplierId" + ] +} diff --git a/src/main/resources/static/bidder-params/aso.json b/src/main/resources/static/bidder-params/aso.json new file mode 100644 index 00000000000..edb3c0feb9c --- /dev/null +++ b/src/main/resources/static/bidder-params/aso.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adserver.Online Adapter Params", + "description": "A schema which validates params accepted by the aso adapter", + "type": "object", + "properties": { + "zone": { + "type": "integer", + "description": "An ID which identifies the zone selling the impression" + } + }, + "required": ["zone"] +} diff --git a/src/main/resources/static/bidder-params/bidmatic.json b/src/main/resources/static/bidder-params/bidmatic.json new file mode 100644 index 00000000000..65a1309dafa --- /dev/null +++ b/src/main/resources/static/bidder-params/bidmatic.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidmatic Adapter Params", + "description": "A schema which validates params accepted by the Bidmatic adapter", + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "source": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": [ + "source" + ] +} diff --git a/src/main/resources/static/bidder-params/bigoad.json b/src/main/resources/static/bidder-params/bigoad.json new file mode 100644 index 00000000000..15d8c5981fa --- /dev/null +++ b/src/main/resources/static/bidder-params/bigoad.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "BigoAd Adapter Params", + "description": "A schema which validates params accepted by the BigoAd adapter", + "type": "object", + "properties": { + "sspid": { + "type": "string", + "description": "Special id provided by BigoAd" + } + }, + "required": [ + "sspid" + ] +} diff --git a/src/main/resources/static/bidder-params/bizzclick.json b/src/main/resources/static/bidder-params/bizzclick.json deleted file mode 100644 index 879ab45314f..00000000000 --- a/src/main/resources/static/bidder-params/bizzclick.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Bizzclick Adapter Params", - "description": "A schema which validates params accepted by the Bizzclick adapter", - "type": "object", - "properties": { - "accountId": { - "type": "string", - "description": "Account id", - "minLength": 1 - }, - "placementId": { - "type": "string", - "description": "PlacementId id", - "minLength": 1 - } - }, - "required": [ - "accountId", - "placementId" - ] -} diff --git a/src/main/resources/static/bidder-params/blasto.json b/src/main/resources/static/bidder-params/blasto.json new file mode 100644 index 00000000000..23109fb2421 --- /dev/null +++ b/src/main/resources/static/bidder-params/blasto.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Blasto Adapter Params", + "description": "A schema which validates params accepted by the Blasto adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "description": "Source id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "sourceId" + ] +} diff --git a/src/main/resources/static/bidder-params/bwx.json b/src/main/resources/static/bidder-params/bwx.json new file mode 100644 index 00000000000..cabe329cdea --- /dev/null +++ b/src/main/resources/static/bidder-params/bwx.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "BoldwinX Adapter Params", + "description": "A schema which validates params accepted by the BoldwinX adapter", + "type": "object", + "properties": { + "pid": { + "type": "string", + "description": "Unique placement ID", + "minLength": 1 + }, + "env": { + "type": "string", + "description": "BoldwinX environment", + "minLength": 1 + } + }, + "required": [ + "pid" + ] +} diff --git a/src/main/resources/static/bidder-params/cointraffic.json b/src/main/resources/static/bidder-params/cointraffic.json new file mode 100644 index 00000000000..8a749fdfac3 --- /dev/null +++ b/src/main/resources/static/bidder-params/cointraffic.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Cointraffic Adapter Params", + "description": "A schema which validates params accepted by the Cointraffic adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Ad placement identifier" + } + }, + "required": [ + "placementId" + ] +} diff --git a/src/main/resources/static/bidder-params/concert.json b/src/main/resources/static/bidder-params/concert.json new file mode 100644 index 00000000000..e29d075d1fe --- /dev/null +++ b/src/main/resources/static/bidder-params/concert.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Concert Adapter Params", + "description": "A schema which validates params accepted by the Concert adapter", + "type": "object", + "properties": { + "partnerId": { + "type": "string", + "description": "The partner id assigned by concert.", + "minLength": 1 + }, + "placementId": { + "type": "integer", + "description": "The placement id." + }, + "site": { + "type": "string", + "description": "The site name." + }, + "slot": { + "type": "string", + "description": "The slot name." + }, + "sizes": { + "type": "array", + "description": "All sizes this ad unit accepts.", + "items": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 2, + "maxItems": 2 + } + } + }, + "required": [ + "partnerId" + ] +} diff --git a/src/main/resources/static/bidder-params/connectad.json b/src/main/resources/static/bidder-params/connectad.json index faed542913f..e36410928da 100644 --- a/src/main/resources/static/bidder-params/connectad.json +++ b/src/main/resources/static/bidder-params/connectad.json @@ -2,15 +2,20 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "ConnectAd S2S dapter Params", "description": "A schema which validates params accepted by the ConnectAd Adapter", - "type": "object", "properties": { "networkId": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "NetworkId" }, "siteId": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "SiteId" }, "bidfloor": { @@ -18,5 +23,8 @@ "description": "Requests Floorprice" } }, - "required": ["networkId", "siteId"] + "required": [ + "networkId", + "siteId" + ] } diff --git a/src/main/resources/static/bidder-params/copper6ssp.json b/src/main/resources/static/bidder-params/copper6ssp.json new file mode 100644 index 00000000000..e17c3f38ce7 --- /dev/null +++ b/src/main/resources/static/bidder-params/copper6ssp.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Copper6SSPs Adapter Params", + "description": "A schema which validates params accepted by the Copper6SSP adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/definemedia.json b/src/main/resources/static/bidder-params/definemedia.json new file mode 100644 index 00000000000..6b25ed0636c --- /dev/null +++ b/src/main/resources/static/bidder-params/definemedia.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Define Media Adapter Params", + "description": "A schema which validates params accepted by the DM adapter", + "type": "object", + "properties": { + "mandantId": { + "type": "integer", + "description": "The DEFINE-MEDIA mandant id. This is a unique identifier for your account. Please contact your account manager for more information." + }, + "adslotId": { + "type": "integer", + "description": "The adslot id. This is a unique identifier for your adslot and may change on subparts on a website. Please contact your account manager for more information." + } + }, + "required": [ + "mandantId" + ] +} diff --git a/src/main/resources/static/bidder-params/displayio.json b/src/main/resources/static/bidder-params/displayio.json new file mode 100644 index 00000000000..4afa23f2108 --- /dev/null +++ b/src/main/resources/static/bidder-params/displayio.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Display.io Adapter Params", + "description": "A schema which validates params accepted by the Display.io adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "Publisher Id" + }, + "inventoryId": { + "type": "string", + "description": "Inventory Id" + }, + "placementId": { + "type": "string", + "description": "Placement Id" + } + }, + "required": [ + "publisherId", + "inventoryId", + "placementId" + ] +} diff --git a/src/main/resources/static/bidder-params/driftpixel.json b/src/main/resources/static/bidder-params/driftpixel.json new file mode 100644 index 00000000000..60ad7efc173 --- /dev/null +++ b/src/main/resources/static/bidder-params/driftpixel.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "DriftPixel Adapter Params", + "description": "A schema which validates params accepted by the DriftPixel adapter", + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "DriftPixel environment", + "minLength": 1 + }, + "pid": { + "type": "string", + "description": "Unique placement ID", + "minLength": 1 + } + }, + "required": [ + "pid" + ] +} diff --git a/src/main/resources/static/bidder-params/escalax.json b/src/main/resources/static/bidder-params/escalax.json new file mode 100644 index 00000000000..68fda39c259 --- /dev/null +++ b/src/main/resources/static/bidder-params/escalax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Escalax Adapter Params", + "description": "A schema which validates params accepted by the Escalax adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "description": "Source id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "sourceId" + ] +} diff --git a/src/main/resources/static/bidder-params/improvedigital.json b/src/main/resources/static/bidder-params/improvedigital.json index 5681d896e92..ecd60a98b1d 100644 --- a/src/main/resources/static/bidder-params/improvedigital.json +++ b/src/main/resources/static/bidder-params/improvedigital.json @@ -35,5 +35,7 @@ "description": "Placement size" } }, - "required": ["placementId"] + "required": [ + "placementId" + ] } diff --git a/src/main/resources/static/bidder-params/liftoff.json b/src/main/resources/static/bidder-params/liftoff.json deleted file mode 100644 index 5664a883b9e..00000000000 --- a/src/main/resources/static/bidder-params/liftoff.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Liftoff Adapter Params", - "description": "A schema which validates params accepted by the Liftoff adapter", - "type": "object", - "properties": { - "app_store_id": { - "type": "string", - "minLength": 1, - "description": "Pub App Store ID" - }, - "placement_reference_id": { - "type": "string", - "minLength": 1, - "description": "Placement Reference ID" - } - }, - "required": [ - "app_store_id", - "placement_reference_id" - ] -} diff --git a/src/main/resources/static/bidder-params/loopme.json b/src/main/resources/static/bidder-params/loopme.json index f6b4a0a8b2e..5ea22ec7ba5 100644 --- a/src/main/resources/static/bidder-params/loopme.json +++ b/src/main/resources/static/bidder-params/loopme.json @@ -3,13 +3,22 @@ "title": "Loopme Adapter Params", "description": "A schema which validates params accepted by the Loopme adapter", "type": "object", - "properties": { - "accountId": { + "publisherId": { "type": "string", - "description": "Account ID" + "description": "An id which identifies Loopme partner", + "minLength": 1 + }, + "bundleId": { + "type": "string", + "description": "An id which identifies app/site in Loopme", + "minLength": 1 + }, + "placementId": { + "type": "string", + "description": "A placement id in Loopme", + "minLength": 1 } }, - - "required": ["accountId"] -} \ No newline at end of file + "required": ["publisherId"] +} diff --git a/src/main/resources/static/bidder-params/loyal.json b/src/main/resources/static/bidder-params/loyal.json new file mode 100644 index 00000000000..3173ac633d3 --- /dev/null +++ b/src/main/resources/static/bidder-params/loyal.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Loyal Adapter Params", + "description": "A schema which validates params accepted by the Loyal adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/mediago.json b/src/main/resources/static/bidder-params/mediago.json new file mode 100644 index 00000000000..a4fcb3b5392 --- /dev/null +++ b/src/main/resources/static/bidder-params/mediago.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MediaGo Adapter Params", + "description": "A schema which validates params accepted by the MediaGo adapter", + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Publisher token,communicate with MediaGo to obtain it. This parameter expects all imps to be the same.", + "minLength": 1 + }, + "region": { + "type": "string", + "enum": ["US", "EU", "APAC"], + "description": "Server region for PBS request: US for US Region, EU for EU Region, APAC for APAC Region, default is US. This parameter expects all imps to be the same" + }, + "placementId": { + "type": "string", + "description": "The AD placement ID.", + "minLength": 1 + } + }, + "required": ["token"] +} diff --git a/src/main/resources/static/bidder-params/melozen.json b/src/main/resources/static/bidder-params/melozen.json new file mode 100644 index 00000000000..eebd391944b --- /dev/null +++ b/src/main/resources/static/bidder-params/melozen.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MeloZen Adapter Params", + "description": "A schema which validates params accepted by the MeloZen adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "minLength": 1, + "description": "The unique identifier for the publisher." + } + }, + "required": [ + "pubId" + ] +} diff --git a/src/main/resources/static/bidder-params/metax.json b/src/main/resources/static/bidder-params/metax.json new file mode 100644 index 00000000000..5e65b5c4e2b --- /dev/null +++ b/src/main/resources/static/bidder-params/metax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MetaX Adapter Params", + "description": "A schema which validates params accepted by the MetaX adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "integer", + "description": "An ID which identifies the publisher", + "minimum": 1 + }, + "adunit": { + "type": "integer", + "description": "An ID which identifies the adunit", + "minimum": 1 + } + }, + "required": [ + "publisherId", + "adunit" + ] +} diff --git a/src/main/resources/static/bidder-params/missena.json b/src/main/resources/static/bidder-params/missena.json new file mode 100644 index 00000000000..86bf5b45dec --- /dev/null +++ b/src/main/resources/static/bidder-params/missena.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Missena Adapter Params", + "description": "A schema which validates params accepted by the Missena adapter", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "API Key", + "minLength": 1 + }, + "placement": { + "type": "string", + "description": "Placement Type (Sticky, Header, ...)" + }, + "test": { + "type": "string", + "description": "Test Mode" + } + }, + "required": [ + "apiKey" + ] +} diff --git a/src/main/resources/static/bidder-params/openweb.json b/src/main/resources/static/bidder-params/openweb.json index ec5766ad663..0550c61e0f1 100644 --- a/src/main/resources/static/bidder-params/openweb.json +++ b/src/main/resources/static/bidder-params/openweb.json @@ -2,25 +2,36 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "OpenWeb Adapter Params", "description": "A schema which validates params accepted by the OpenWeb adapter", - "type": "object", "properties": { "placementId": { - "type": "integer", - "description": "An ID which identifies this placement of the impression" - }, - "siteId": { - "type": "integer", - "description": "An ID which identifies the site selling the impression" + "type": "string", + "description": "An ID which identifies this placement of the impression", + "minLength": 1 }, "aid": { "type": "integer", - "description": "An ID which identifies the channel" + "description": "Deprecated: An ID which identifies the channel" }, - "bidFloor": { - "type": "number", - "description": "BidFloor, US Dollars" + "org": { + "type": "string", + "description": "The organization ID.", + "minLength": 1 } }, - "required": ["aid"] + "required": [ + "placementId" + ], + "oneOf": [ + { + "required": [ + "aid" + ] + }, + { + "required": [ + "org" + ] + } + ] } diff --git a/src/main/resources/static/bidder-params/openx.json b/src/main/resources/static/bidder-params/openx.json index 6dbd10178e4..89c0663e0a0 100644 --- a/src/main/resources/static/bidder-params/openx.json +++ b/src/main/resources/static/bidder-params/openx.json @@ -6,7 +6,7 @@ "type": "object", "properties": { "unit": { - "type": "string", + "type": ["number", "string"], "description": "The ad unit id.", "pattern": "^[0-9]+$" }, @@ -22,9 +22,10 @@ "format": "uuid" }, "customFloor": { - "type": "number", + "type": ["number", "string"], "description": "The minimum CPM price in USD.", - "minimum": 0 + "minimum": 0, + "pattern": "^[0-9]+(\\.[0-9]+)?$" }, "customParams": { "type": "object", diff --git a/src/main/resources/static/bidder-params/oraki.json b/src/main/resources/static/bidder-params/oraki.json new file mode 100644 index 00000000000..9a2d596eeff --- /dev/null +++ b/src/main/resources/static/bidder-params/oraki.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Oraki Adapter Params", + "description": "A schema which validates params accepted by the Oraki adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/ownadx.json b/src/main/resources/static/bidder-params/ownadx.json new file mode 100644 index 00000000000..fae8689bf55 --- /dev/null +++ b/src/main/resources/static/bidder-params/ownadx.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "OwnAdx Adapter Params", + "description": "A schema which validates params accepted by the OwnAdx adapter", + "type": "object", + "properties": { + "sspId": { + "type": "string", + "description": "Ssp ID" + }, + "seatId": { + "type": "string", + "description": "Seat ID" + }, + "tokenId": { + "type": "string", + "description": "Token ID" + } + }, + "required": [ + "sspId", + "seatId", + "tokenId" + ] +} diff --git a/src/main/resources/static/bidder-params/playdigo.json b/src/main/resources/static/bidder-params/playdigo.json new file mode 100644 index 00000000000..8611f216a9e --- /dev/null +++ b/src/main/resources/static/bidder-params/playdigo.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Playdigo Adapter Params", + "description": "A schema which validates params accepted by the Playdigo adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/pubrise.json b/src/main/resources/static/bidder-params/pubrise.json new file mode 100644 index 00000000000..9dd2a1e4c80 --- /dev/null +++ b/src/main/resources/static/bidder-params/pubrise.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Pubrise Adapter Params", + "description": "A schema which validates params accepted by the Pubrise adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/pulsepoint.json b/src/main/resources/static/bidder-params/pulsepoint.json index 7758a67084d..8650a96a2f7 100644 --- a/src/main/resources/static/bidder-params/pulsepoint.json +++ b/src/main/resources/static/bidder-params/pulsepoint.json @@ -5,11 +5,17 @@ "type": "object", "properties": { "cp": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "An ID which identifies the publisher selling the impression" }, "ct": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "An ID which identifies the ad slot being sold" } }, diff --git a/src/main/resources/static/bidder-params/qt.json b/src/main/resources/static/bidder-params/qt.json new file mode 100644 index 00000000000..ef7eb77a9ac --- /dev/null +++ b/src/main/resources/static/bidder-params/qt.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "QT Adapter Params", + "description": "A schema which validates params accepted by the QT adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/readpeak.json b/src/main/resources/static/bidder-params/readpeak.json new file mode 100644 index 00000000000..274aadb92e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/readpeak.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Readpeak Adapter Params", + "description": "A schema which validates params accepted by the Readpeak adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "Publisher ID provided by Readpeak" + }, + "siteId": { + "type": "string", + "description": "Site/Media ID provided by Readpeak" + }, + "bidfloor": { + "type": "number", + "description": "CPM Bid Floor" + }, + "tagId": { + "type": "string", + "description": "Ad placement identifier" + } + }, + "required": ["publisherId"] +} diff --git a/src/main/resources/static/bidder-params/rise.json b/src/main/resources/static/bidder-params/rise.json index 30dff44d29f..ee8a469cbbc 100644 --- a/src/main/resources/static/bidder-params/rise.json +++ b/src/main/resources/static/bidder-params/rise.json @@ -11,6 +11,10 @@ "publisher_id": { "type": "string", "description": "Deprecated, use org instead." + }, + "placementId": { + "type": "string", + "description": "Placement ID." } }, "oneOf": [ diff --git a/src/main/resources/static/bidder-params/roulax.json b/src/main/resources/static/bidder-params/roulax.json new file mode 100644 index 00000000000..ce7af170136 --- /dev/null +++ b/src/main/resources/static/bidder-params/roulax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Roulax Adapter Params", + "description": "A schema which validates params accepted by the Roulax adapter", + "type": "object", + "properties": { + "Pid": { + "type": "string", + "minLength": 1, + "description": "PID" + }, + "PublisherPath": { + "type": "string", + "minLength": 1, + "description": "PublisherPath" + } + }, + "required": [ + "Pid", + "PublisherPath" + ] +} diff --git a/src/main/resources/static/bidder-params/seedingAlliance.json b/src/main/resources/static/bidder-params/seedingAlliance.json index 52c3d30e087..ceb53dae3f3 100644 --- a/src/main/resources/static/bidder-params/seedingAlliance.json +++ b/src/main/resources/static/bidder-params/seedingAlliance.json @@ -11,7 +11,11 @@ }, "seatId": { "type": "string", - "description": "Seat ID" + "description": "Deprecated, please use accountId" + }, + "accountId": { + "type": "string", + "description": "Account ID of partner" } }, "required": [ diff --git a/src/main/resources/static/bidder-params/smrtconnect.json b/src/main/resources/static/bidder-params/smrtconnect.json new file mode 100644 index 00000000000..74229a46133 --- /dev/null +++ b/src/main/resources/static/bidder-params/smrtconnect.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smrtconnect Params", + "description": "A schema which validates params accepted by the Smrtconnect", + "type": "object", + "properties": { + "supply_id": { + "type": "string", + "description": "Supply id", + "minLength": 1 + } + }, + "required": ["supply_id"] +} diff --git a/src/main/resources/static/bidder-params/sovrn.json b/src/main/resources/static/bidder-params/sovrn.json index 803a8e127a1..4f779a9f1f6 100644 --- a/src/main/resources/static/bidder-params/sovrn.json +++ b/src/main/resources/static/bidder-params/sovrn.json @@ -13,8 +13,16 @@ "description": "An ID which identifies the sovrn ad tag (DEPRECATED, use \"tagid\" instead)" }, "bidfloor": { - "type": "number", - "description": "The minimum acceptable bid, in CPM, using US Dollars" + "anyOf": [ + { + "type": "number", + "description": "The minimum acceptable bid, in CPM, using US Dollars" + }, + { + "type": "string", + "description": "The minimum acceptable bid, in CPM, using US Dollars (as a string)" + } + ] }, "adunitcode": { "type": "string", diff --git a/src/main/resources/static/bidder-params/theadx.json b/src/main/resources/static/bidder-params/theadx.json new file mode 100644 index 00000000000..b6e0adca99b --- /dev/null +++ b/src/main/resources/static/bidder-params/theadx.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Theadx Adapter Params", + "description": "A schema which validates params accepted by the theadx adapter", + "type": "object", + "properties": { + "pid": { + "type": [ + "integer", + "string" + ], + "pattern": "^\\d+$", + "description": "An ID which identifies the partner selling the impression" + }, + "tagid": { + "type": [ + "integer", + "string" + ], + "pattern": "^\\d+$", + "description": "An ID which identifies the placement selling the impression" + }, + "wid": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the Theadx inventory source id" + } + }, + "anyOf": [ + { + "required": [ + "tagid" + ] + }, + { + "required": [ + "pid", + "wid", + "tagid" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/thetradedesk.json b/src/main/resources/static/bidder-params/thetradedesk.json new file mode 100644 index 00000000000..d0b305a5a1e --- /dev/null +++ b/src/main/resources/static/bidder-params/thetradedesk.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "The Trade Desk Adapter Params", + "description": "A schema which validates params accepted by the The Trade Desk adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "An ID which identifies the publisher" + } + }, + "required": [ + "publisherId" + ] +} diff --git a/src/main/resources/static/bidder-params/tradplus.json b/src/main/resources/static/bidder-params/tradplus.json new file mode 100644 index 00000000000..deae1392d1d --- /dev/null +++ b/src/main/resources/static/bidder-params/tradplus.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "TradPlus Adapter Params", + "description": "A schema which validates params accepted by the TradPlus adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone ID" + } + }, + "required": [ + "accountId", + "zoneId" + ] +} diff --git a/src/main/resources/static/bidder-params/triplelift_native.json b/src/main/resources/static/bidder-params/triplelift_native.json index 9afcfa66279..4cf90ef49e7 100644 --- a/src/main/resources/static/bidder-params/triplelift_native.json +++ b/src/main/resources/static/bidder-params/triplelift_native.json @@ -2,16 +2,15 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Triplelift Adapter Params", "description": "A schema which validates params accepted by the Triplelift adapter", + "type": "object", "properties": { "inventoryCode": { "type": "string", + "minLength": 1, "description": "TripleLift inventory code for this ad unit (provided to you by your partner manager)" }, - "floor": { - "description": "the bid floor, in usd", - "type": "number" - } + "floor" : {"description" : "the bid floor, in usd", "type": "number" } }, "required": ["inventoryCode"] } diff --git a/src/main/resources/static/bidder-params/trustedstack.json b/src/main/resources/static/bidder-params/trustedstack.json new file mode 100644 index 00000000000..8da4b5cbc80 --- /dev/null +++ b/src/main/resources/static/bidder-params/trustedstack.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Trustedstack Adapter Params", + "description": "A schema which validates params accepted by the Trustedstack adapter", + "type": "object", + "properties": { + "cid": { + "type": "string", + "description": "The customer id provided by Trustedstack." + }, + "crid": { + "type": "string", + "description": "The placement id provided by Trustedstack." + } + }, + "required": [ + "cid", + "crid" + ] +} diff --git a/src/main/resources/static/bidder-params/vidazoo.json b/src/main/resources/static/bidder-params/vidazoo.json new file mode 100644 index 00000000000..658b4fdc26e --- /dev/null +++ b/src/main/resources/static/bidder-params/vidazoo.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Vidazoo Adapter Params", + "description": "A schema which validates params accepted by the Vidazoo adapter", + "type": "object", + "properties": { + "cId": { + "type": "string", + "description": "The connection id.", + "minLength": 1 + } + }, + "required": [ + "cId" + ] +} diff --git a/src/main/resources/static/bidder-params/vungle.json b/src/main/resources/static/bidder-params/vungle.json new file mode 100644 index 00000000000..e2d4dddffdc --- /dev/null +++ b/src/main/resources/static/bidder-params/vungle.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Vungle Adapter Params", + "description": "A schema which validates params accepted by the Vungle adapter", + "type": "object", + "properties": { + "app_store_id": { + "type": "string", + "minLength": 1, + "description": "Pub App Store ID" + }, + "placement_reference_id": { + "type": "string", + "minLength": 1, + "description": "Placement Reference ID" + } + }, + "required": [ + "app_store_id", + "placement_reference_id" + ] +} diff --git a/src/main/resources/static/bidder-params/yandex.json b/src/main/resources/static/bidder-params/yandex.json index 66f41fbc241..43413a66867 100644 --- a/src/main/resources/static/bidder-params/yandex.json +++ b/src/main/resources/static/bidder-params/yandex.json @@ -7,16 +7,32 @@ "page_id": { "type": "integer", "minLength": 1, + "minimum": 1, "description": "Special Page Id provided by Yandex Manager" }, "imp_id": { "type": "integer", "minLength": 1, + "minimum": 1, "description": "Special identifier provided by Yandex Manager" + }, + "placement_id": { + "type": "string", + "description": "Ad placement identifier", + "pattern": "(\\S+-)?\\d+-\\d+" } }, - "required": [ - "page_id", - "imp_id" + "oneOf": [ + { + "required": [ + "page_id", + "imp_id" + ] + }, + { + "required": [ + "placement_id" + ] + } ] } diff --git a/src/main/resources/static/bidder-params/yieldlab.json b/src/main/resources/static/bidder-params/yieldlab.json index 900d65da6e5..9d0fd0e88c0 100644 --- a/src/main/resources/static/bidder-params/yieldlab.json +++ b/src/main/resources/static/bidder-params/yieldlab.json @@ -12,10 +12,6 @@ "type": "string", "description": "Yieldlab ID of the supply" }, - "adSize": { - "type": "string", - "description": "Size of the adslot in pixel, e.g. 200x50" - }, "extId": { "type": "string", "description": "External ID used for reporting" @@ -27,7 +23,6 @@ }, "required": [ "adslotId", - "supplyId", - "adSize" + "supplyId" ] } diff --git a/src/main/resources/static/bidder-params/zmaticoo.json b/src/main/resources/static/bidder-params/zmaticoo.json new file mode 100644 index 00000000000..8200c94d13c --- /dev/null +++ b/src/main/resources/static/bidder-params/zmaticoo.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "zMaticoo Adapter Params", + "description": "A schema which validates params accepted by the zMaticoo adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "description": "Publisher ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone Id", + "minLength": 1 + } + }, + "required": [ + "pubId", + "zoneId" + ] +} diff --git a/src/test/groovy/org/prebid/server/functional/model/Currency.groovy b/src/test/groovy/org/prebid/server/functional/model/Currency.groovy index cd3360510fa..fb48fc4a6d1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/Currency.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/Currency.groovy @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum Currency { - USD, EUR, GBP, JPY, BOGUS + USD, EUR, GBP, JPY, CHF, CAD, BOGUS @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index e41bdeb98fe..5efcdf40709 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonValue enum ModuleName { - PB_RICHMEDIA_FILTER("pb-richmedia-filter") + PB_RICHMEDIA_FILTER("pb-richmedia-filter"), + PB_RESPONSE_CORRECTION ("pb-response-correction"), + ORTB2_BLOCKING("ortb2-blocking") @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy index a27e542b127..f91f209395c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy @@ -5,9 +5,12 @@ import net.minidev.json.annotate.JsonIgnore enum BidderName { + WILDCARD("*"), UNKNOWN("unknown"), + EMPTY(""), BOGUS("bogus"), ALIAS("alias"), + ALIAS_CAMEL_CASE("AlIaS"), GENERIC_CAMEL_CASE("GeNerIc"), GENERIC("generic"), RUBICON("rubicon"), @@ -18,6 +21,7 @@ enum BidderName { ACUITYADS("acuityads"), AAX("aax"), ADKERNEL("adkernel"), + IX("ix"), GRID("grid"), MEDIANET("medianet") diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy new file mode 100644 index 00000000000..3184fb17fee --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy @@ -0,0 +1,8 @@ +package org.prebid.server.functional.model.bidder + +class GeneralBidderAdapter extends Generic { + + String siteId + List size + String sid +} diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/Generic.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/Generic.groovy index 3d67f9ae687..f792bdf00c8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/Generic.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/Generic.groovy @@ -1,7 +1,9 @@ package org.prebid.server.functional.model.bidder import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode +@EqualsAndHashCode class Generic implements BidderAdapter { Object exampleProperty diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/Openx.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/Openx.groovy index e6b08baa4c2..932d3cf80a6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/Openx.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/Openx.groovy @@ -7,7 +7,7 @@ class Openx implements BidderAdapter { String unit String delDomain String platform - Integer customFloor + String customFloor Map customParams static Openx getDefaultOpenx() { diff --git a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy index 62799412692..c1889dc51bc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.bidderspecific +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.Imp +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class BidderImp extends Imp { diff --git a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy index e0e3b26d02a..d07ca31ad9d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy @@ -1,12 +1,12 @@ package org.prebid.server.functional.model.bidderspecific import groovy.transform.ToString -import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.GeneralBidderAdapter import org.prebid.server.functional.model.request.auction.ImpExt @ToString(includeNames = true, ignoreNulls = true) class BidderImpExt extends ImpExt { - Generic bidder + GeneralBidderAdapter bidder Rp rp } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy index fdbc492b93b..0a15cead562 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -9,4 +10,9 @@ import groovy.transform.ToString class AccountAnalyticsConfig { Map auctionEvents + Boolean allowClientDetails + AnalyticsModule modules + + @JsonProperty("auction_events") + Map auctionEventsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 30eea8df410..2983ac3f731 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -12,18 +12,35 @@ import org.prebid.server.functional.model.response.auction.MediaType @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountAuctionConfig { - String priceGranularity + PriceGranularityType priceGranularity Integer bannerCacheTtl Integer videoCacheTtl Integer truncateTargetAttr String defaultIntegration + Boolean debugAllow AccountBidValidationConfig bidValidations AccountEventsConfig events - Boolean debugAllow AccountPriceFloorsConfig priceFloors Targeting targeting @JsonProperty("preferredmediatype") Map preferredMediaType @JsonProperty("privacysandbox") PrivacySandbox privacySandbox + + @JsonProperty("price_granularity") + PriceGranularityType priceGranularitySnakeCase + @JsonProperty("banner_cache_ttl") + Integer bannerCacheTtlSnakeCase + @JsonProperty("video_cache_ttl") + Integer videoCacheTtlSnakeCase + @JsonProperty("truncate_target_attr") + Integer truncateTargetAttrSnakeCase + @JsonProperty("default_integration") + String defaultIntegrationSnakeCase + @JsonProperty("debug_allow") + Boolean debugAllowSnakeCase + @JsonProperty("bid_validation") + AccountBidValidationConfig bidValidationsSnakeCase + @JsonProperty("price_floors") + AccountPriceFloorsConfig priceFloorsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountBidValidationConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountBidValidationConfig.groovy index eb6cd4fe882..f90d18b4fbf 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountBidValidationConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountBidValidationConfig.groovy @@ -8,4 +8,6 @@ class AccountBidValidationConfig { @JsonProperty("banner-creative-max-size") BidValidationEnforcement bannerMaxSizeEnforcement + @JsonProperty("banner_creative_max_size") + BidValidationEnforcement bannerMaxSizeEnforcementSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy index 52530fef872..b3189d40243 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountCcpaConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -11,4 +12,6 @@ class AccountCcpaConfig { Boolean enabled Map channelEnabled + @JsonProperty("channel_enabled") + Map channelEnabledSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy index 08bb7b4d1cf..60563ac2f5e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.EqualsAndHashCode @@ -19,6 +20,9 @@ class AccountConfig { AccountMetricsConfig metrics AccountCookieSyncConfig cookieSync AccountHooksConfiguration hooks + AccountSetting settings + @JsonProperty("cookie_sync") + AccountCookieSyncConfig cookieSyncSnakeCase static getDefaultAccountConfig() { new AccountConfig(status: AccountStatus.ACTIVE) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountCookieSyncConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountCookieSyncConfig.groovy index 5dbaa4399a0..e86227ec297 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountCookieSyncConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountCookieSyncConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -12,4 +13,11 @@ class AccountCookieSyncConfig { Integer maxLimit List pri AccountCoopSyncConfig coopSync + + @JsonProperty("default_limit") + Integer defaultLimitSnakeCase + @JsonProperty("max_limit") + Integer maxLimitSnakeCase + @JsonProperty("coop_sync") + AccountCoopSyncConfig coopSyncSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy index ef134c916fd..a77b082523a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountDsaConfig.groovy @@ -13,4 +13,6 @@ class AccountDsaConfig { @JsonProperty("default") Dsa defaultDsa Boolean gdprOnly + @JsonProperty("gdpr_only") + Boolean gdprOnlySnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy index ba1ec11c608..f2f549f003a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountGdprConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -10,9 +11,18 @@ import org.prebid.server.functional.model.ChannelType class AccountGdprConfig { Boolean enabled + String eeaCountries Map channelEnabled + @JsonProperty("channel_enabled") + Map channelEnabledSnakeCase Map purposes Map specialFeatures + @JsonProperty("special_features") + Map specialFeaturesSnakeCase PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation + @JsonProperty("purpose_one_treatment_interpretation") + PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretationSnakeCase List basicEnforcementVendors + @JsonProperty("basic_enforcement_vendors") + List basicEnforcementVendorsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy index af7d723caf5..24f3ab97d77 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -9,5 +10,7 @@ import groovy.transform.ToString class AccountHooksConfiguration { ExecutionPlan executionPlan + @JsonProperty("execution_plan") + ExecutionPlan executionPlanSnakeCase PbsModulesConfig modules } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountMetricsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountMetricsConfig.groovy index 30a7bdc6f81..570c2daf296 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountMetricsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountMetricsConfig.groovy @@ -1,10 +1,13 @@ package org.prebid.server.functional.model.config import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming - +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountMetricsConfig { - @JsonProperty("verbosity-level") - AccountMetricsVerbosityLevel verbosityLevel; + AccountMetricsVerbosityLevel verbosityLevel + @JsonProperty("verbosity_level") + AccountMetricsVerbosityLevel verbosityLevelSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy index 0869421a671..28f908aba44 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -14,4 +15,13 @@ class AccountPriceFloorsConfig { Boolean adjustForBidAdjustment Boolean enforceDealFloors Boolean useDynamicData + + @JsonProperty("enforce_floors_rate") + Integer enforceFloorsRateSnakeCase + @JsonProperty("adjust_for_bid_adjustment") + Boolean adjustForBidAdjustmentSnakeCase + @JsonProperty("enforce_deal_floors") + Boolean enforceDealFloorsSnakeCase + @JsonProperty("use_dynamic_data") + Boolean useDynamicDataSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountSetting.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountSetting.groovy new file mode 100644 index 00000000000..3e123801a56 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountSetting.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AccountSetting { + + Boolean geoLookup + @JsonProperty("geo_lookup") + Boolean geoLookupSnakeCase +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy new file mode 100644 index 00000000000..5b53fedbe8d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AnalyticsModule { + + LogAnalytics logAnalytics +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy new file mode 100644 index 00000000000..6486e292ed5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AppVideoHtml { + + Boolean enabled + List excludedBidders +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy index 3269f0b13f4..b73f4fcaeb3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy @@ -8,7 +8,8 @@ class EndpointExecutionPlan { Map stages - static EndpointExecutionPlan getModuleEndpointExecutionPlan(ModuleName name, Stage stage) { - new EndpointExecutionPlan(stages: [(stage): StageExecutionPlan.getModuleStageExecutionPlan(name, stage)]) + static EndpointExecutionPlan getModuleEndpointExecutionPlan(ModuleName name, List stages) { + new EndpointExecutionPlan(stages: stages.collectEntries { + it -> [(it): StageExecutionPlan.getModuleStageExecutionPlan(name, it)] } as Map) } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy index d0a1af281e6..09e95d7753d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionGroup.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -12,7 +13,13 @@ class ExecutionGroup { Long timeout List hookSequence + @JsonProperty("hook_sequence") + List hookSequenceSnakeCase + static ExecutionGroup getModuleExecutionGroup(ModuleName name, Stage stage) { - new ExecutionGroup(timeout: 100, hookSequence: [new HookId(moduleCode: name.code, hookImplCode: "${name.code}-${stage.value}-hook")]) + new ExecutionGroup().tap { + timeout = 100 + hookSequence = [new HookId(moduleCode: name.code, hookImplCode: ModuleHookImplementation.forValue(name, stage).code)] + } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy index 0846c626888..766139bc5c5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy @@ -8,7 +8,7 @@ class ExecutionPlan { Map endpoints - static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, ModuleName moduleName, Stage stage) { + static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, ModuleName moduleName, List stage) { new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModuleEndpointExecutionPlan(moduleName, stage)]) } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/GppModuleConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/GppModuleConfig.groovy index 3ac38525dc7..0d10144e0bf 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/GppModuleConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/GppModuleConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString import org.prebid.server.functional.model.request.GppSectionId @@ -7,12 +8,24 @@ import org.prebid.server.functional.model.request.GppSectionId class GppModuleConfig { List activityConfig + @JsonProperty("activity_config") + List activityConfigSnakeCase + @JsonProperty("activity-config") + List activityConfigKebabCase List sids Boolean normalizeFlags + @JsonProperty("normalize_flags") + Boolean normalizeFlagsSnakeCase + @JsonProperty("normalize-flags") + Boolean normalizeFlagsKebabCase List skipSids + @JsonProperty("skip_sids") + List skipSidsSnakeCase + @JsonProperty("skip-sids") + List skipSidsKebabCase static GppModuleConfig getDefaultModuleConfig(ActivityConfig activityConfig = ActivityConfig.configWithDefaultRestrictRules, - List sids = [GppSectionId.USP_NAT_V1], + List sids = [GppSectionId.US_NAT_V1], Boolean normalizeFlags = true) { new GppModuleConfig().tap { it.activityConfig = [activityConfig] @@ -20,4 +33,24 @@ class GppModuleConfig { it.normalizeFlags = normalizeFlags } } + + static GppModuleConfig getDefaultModuleConfigSnakeCase(ActivityConfig activityConfig = ActivityConfig.configWithDefaultRestrictRules, + List sids = [GppSectionId.US_NAT_V1], + Boolean normalizeFlags = true) { + new GppModuleConfig().tap { + it.activityConfigSnakeCase = [activityConfig] + it.sids = sids + it.normalizeFlagsSnakeCase = normalizeFlags + } + } + + static GppModuleConfig getDefaultModuleConfigKebabCase(ActivityConfig activityConfig = ActivityConfig.configWithDefaultRestrictRules, + List sids = [GppSectionId.US_NAT_V1], + Boolean normalizeFlags = true) { + new GppModuleConfig().tap { + it.activityConfigKebabCase = [activityConfig] + it.sids = sids + it.normalizeFlagsKebabCase = normalizeFlags + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/HookId.groovy b/src/test/groovy/org/prebid/server/functional/model/config/HookId.groovy index cda44911ce4..4d0e90c67ac 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/HookId.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/HookId.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -10,4 +11,9 @@ class HookId { String moduleCode String hookImplCode + + @JsonProperty("module_code") + String moduleCodeSnakeCase + @JsonProperty("hook_impl_code") + String hookImplCodeSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy b/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy new file mode 100644 index 00000000000..cc6d9cc033a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class LogAnalytics { + + Boolean enabled + String additionalData +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy new file mode 100644 index 00000000000..b5c57122a3f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.ModuleName + +//TODO remove if module hooks implementation codes will become consistent +enum ModuleHookImplementation { + + PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"), + RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses"), + ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), + ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") + + @JsonValue + final String code + + ModuleHookImplementation(String code) { + this.code = code + } + + static ModuleHookImplementation forValue(ModuleName name, Stage stage) { + values().find { it.code.contains(name.code) && it.code.contains(stage.value) } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy new file mode 100644 index 00000000000..de8eae06d04 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy @@ -0,0 +1,76 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingActionOverride { + + List enforceBlocks + List blockedAdomain + List blockedApp + List blockedBannerAttr + List blockedVideoAttr + List blockedAudioAttr + List blockedAdvCat + List blockedBannerType + + List blockUnknownAdomain + List blockUnknownAdvCat + + List allowedAdomainForDeals + List allowedAppForDeals + List allowedBannerAttrForDeals + List allowedVideoAttrForDeals + List allowedAudioAttrForDeals + List allowedAdvCatForDeals + + static Ortb2BlockingActionOverride getDefaultOverride(Ortb2BlockingAttribute attribute, + List blocked, + List allowedForDeals = null) { + + new Ortb2BlockingActionOverride().tap { + switch (attribute) { + case BADV: + blockedAdomain = blocked + allowedAdomainForDeals = allowedForDeals + break + case BAPP: + blockedApp = blocked + allowedAppForDeals = allowedForDeals + break + case BANNER_BATTR: + blockedBannerAttr = blocked + allowedBannerAttrForDeals = allowedForDeals + break + case VIDEO_BATTR: + blockedVideoAttr = blocked + allowedVideoAttrForDeals = allowedForDeals + break + case AUDIO_BATTR: + blockedAudioAttr = blocked + allowedAudioAttrForDeals = allowedForDeals + break + case BCAT: + blockedAdvCat = blocked + allowedAdvCatForDeals = allowedForDeals + break + case BTYPE: + blockedBannerType = blocked + break + default: + throw new IllegalArgumentException("Unknown attribute type: $attribute") + } + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy new file mode 100644 index 00000000000..15c54c2c021 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum Ortb2BlockingAttribute { + + BADV('badv'), + BAPP('bapp'), + BANNER_BATTR('battr'), + VIDEO_BATTR('battr'), + AUDIO_BATTR('battr'), + BCAT('bcat'), + BTYPE('btype') + + @JsonValue + final String value + + Ortb2BlockingAttribute(String value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy new file mode 100644 index 00000000000..9e622472024 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy @@ -0,0 +1,78 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingAttributeConfig { + + Boolean enforceBlocks + + Object blockedAdomain + Object blockedApp + Object blockedBannerAttr + Object blockedVideoAttr + Object blockedAudioAttr + Object blockedAdvCat + Object blockedBannerType + + Object blockUnknownAdomain + Object blockUnknownAdvCat + + Object allowedAdomainForDeals + Object allowedAppForDeals + Object allowedBannerAttrForDeals + Object allowedVideoAttrForDeals + Object allowedAudioAttrForDeals + Object allowedAdvCatForDeals + + Ortb2BlockingActionOverride actionOverrides + + static getDefaultConfig(Object ortb2Attributes, Ortb2BlockingAttribute attributeName, Object ortb2AttributesForDeals = null) { + new Ortb2BlockingAttributeConfig().tap { + enforceBlocks = false + switch (attributeName) { + case BADV: + blockedAdomain = ortb2Attributes + allowedAdomainForDeals = ortb2AttributesForDeals + break + case BAPP: + blockedApp = ortb2Attributes + allowedAppForDeals = ortb2AttributesForDeals + break + case BANNER_BATTR: + blockedBannerAttr = ortb2Attributes + allowedBannerAttrForDeals = ortb2AttributesForDeals + break + case VIDEO_BATTR: + blockedVideoAttr = ortb2Attributes + allowedVideoAttrForDeals = ortb2AttributesForDeals + break + case AUDIO_BATTR: + blockedAudioAttr = ortb2Attributes + allowedAudioAttrForDeals = ortb2AttributesForDeals + break + case BCAT: + blockedAdvCat = ortb2Attributes + allowedAdvCatForDeals = ortb2AttributesForDeals + break + case BTYPE: + blockedBannerType = ortb2Attributes + break + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } + } + +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy new file mode 100644 index 00000000000..6e983374577 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.response.auction.MediaType + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingConditions { + + List bidders + List mediaType + List dealIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy new file mode 100644 index 00000000000..1cef82cbe52 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Ortb2BlockingConfig { + + Map attributes +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy new file mode 100644 index 00000000000..987aa11e421 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingOverride { + + Object override + Ortb2BlockingConditions conditions +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy new file mode 100644 index 00000000000..46af75deac6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class PbResponseCorrection { + + Boolean enabled + AppVideoHtml appVideoHtml +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 33b2e1478ad..f9121ae0b3a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -10,4 +10,6 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter class PbsModulesConfig { RichmediaFilter pbRichmediaFilter + Ortb2BlockingConfig ortb2Blocking + PbResponseCorrection pbResponseCorrection } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy index 3fd56222ab7..1501f2e1366 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -11,8 +12,18 @@ class PriceFloorsFetch { Boolean enabled String url Long timeoutMs + @JsonProperty("timeout_ms") + Long timeoutMsSnakeCase Long maxFileSizeKb + @JsonProperty("max_file_size_kb") + Long maxFileSizeKbSnakeCase Integer maxRules + @JsonProperty("max_rules") + Integer maxRulesSnakeCase Integer maxAgeSec + @JsonProperty("max_age_sec") + Integer maxAgeSecSnakeCase Integer periodSec + @JsonProperty("period_sec") + Integer periodSecSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy new file mode 100644 index 00000000000..957a2d880bf --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.Range + +enum PriceGranularityType { + + LOW(2, [Range.getDefault(5, 0.5)]), + MEDIUM(2, [Range.getDefault(20, 0.1)]), + MED(2, [Range.getDefault(20, 0.1)]), + HIGH(2, [Range.getDefault(20, 0.01)]), + AUTO(2, [Range.getDefault(5, 0.05), Range.getDefault(10, 0.1), Range.getDefault(20, 0.5)]), + DENSE(2, [Range.getDefault(3, 0.01), Range.getDefault(8, 0.05), Range.getDefault(20, 0.5)]), + UNKNOWN(null, []) + + final Integer precision + final List ranges + + PriceGranularityType(Integer precision, List ranges) { + this.precision = precision + this.ranges = ranges + } + + @JsonValue + String toLowerCase() { + return name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PurposeConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PurposeConfig.groovy index 0ad1a0bdf79..5c6f0592869 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PurposeConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PurposeConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -9,7 +10,13 @@ import groovy.transform.ToString class PurposeConfig { PurposeEnforcement enforcePurpose + @JsonProperty("enforce_purpose") + PurposeEnforcement enforcePurposeSnakeCase Boolean enforceVendors + @JsonProperty("enforce_vendors") + Boolean enforceVendorsSnakeCase List vendorExceptions + @JsonProperty("vendor_exceptions") + List vendorExceptionsSnakeCase PurposeEid eid } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PurposeEid.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PurposeEid.groovy index 23fb7b519b7..51cb16c71bd 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PurposeEid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PurposeEid.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -11,4 +12,8 @@ class PurposeEid { Boolean requireConsent List exceptions Boolean activityTransition + @JsonProperty("require-consent") + Boolean requireConsentKebabCase + @JsonProperty("activity-transition") + Boolean activityTransitionKebabCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PurposeOneTreatmentInterpretation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PurposeOneTreatmentInterpretation.groovy index 427b3b630fe..92c54e9feda 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PurposeOneTreatmentInterpretation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PurposeOneTreatmentInterpretation.groovy @@ -2,6 +2,8 @@ package org.prebid.server.functional.model.config import com.fasterxml.jackson.annotation.JsonValue import groovy.transform.ToString +import org.prebid.server.functional.util.Case +import org.prebid.server.functional.util.PBSUtils @ToString enum PurposeOneTreatmentInterpretation { @@ -10,15 +12,18 @@ enum PurposeOneTreatmentInterpretation { NO_ACCESS_ALLOWED("no-access-allowed"), ACCESS_ALLOWED("access-allowed") - @JsonValue final String value PurposeOneTreatmentInterpretation(String value) { this.value = value } - @Override + @JsonValue String toString() { - value + def type = PBSUtils.getRandomEnum(Case.class) + if (type.SNAKE) { + return PBSUtils.convertCase(value, Case.SNAKE) + } + return value } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/SpecialFeatureConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/SpecialFeatureConfig.groovy index c02f0406dcc..fc0d90ca6f8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/SpecialFeatureConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/SpecialFeatureConfig.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.config +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @@ -10,4 +11,6 @@ class SpecialFeatureConfig { Boolean enforce List vendorExceptions + @JsonProperty("vendor_exceptions") + List vendorExceptionsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/UsNationalPrivacySection.groovy b/src/test/groovy/org/prebid/server/functional/model/config/UsNationalPrivacySection.groovy index caab68873e0..d5733707955 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/UsNationalPrivacySection.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/UsNationalPrivacySection.groovy @@ -1,40 +1,39 @@ package org.prebid.server.functional.model.config import com.fasterxml.jackson.annotation.JsonValue -import com.iab.gpp.encoder.field.UspNatV1Field -import org.prebid.server.functional.util.PBSUtils +import com.iab.gpp.encoder.field.UsNatV1Field enum UsNationalPrivacySection { - SHARING_NOTICE(UspNatV1Field.SHARING_NOTICE), - SALE_OPT_OUT_NOTICE(UspNatV1Field.SALE_OPT_OUT_NOTICE), - SHARING_OPT_OUT_NOTICE(UspNatV1Field.SHARING_OPT_OUT_NOTICE), - TARGETED_ADVERTISING_OPT_OUT_NOTICE(UspNatV1Field.TARGETED_ADVERTISING_OPT_OUT_NOTICE), - SENSITIVE_DATA_PROCESSING_OPT_OUT_NOTICE(UspNatV1Field.SENSITIVE_DATA_PROCESSING_OPT_OUT_NOTICE), - SENSITIVE_DATA_LIMIT_USE_NOTICE(UspNatV1Field.SENSITIVE_DATA_LIMIT_USE_NOTICE), - SALE_OPT_OUT(UspNatV1Field.SALE_OPT_OUT), - SHARING_OPT_OUT(UspNatV1Field.SHARING_OPT_OUT), - TARGETED_ADVERTISING_OPT_OUT(UspNatV1Field.TARGETED_ADVERTISING_OPT_OUT), - SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 1), - SENSITIVE_DATA_RELIGIOUS_BELIEFS(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 2), - SENSITIVE_DATA_HEALTH_INFO(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 3), - SENSITIVE_DATA_ORIENTATION(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 4), - SENSITIVE_DATA_CITIZENSHIP_STATUS(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 5), - SENSITIVE_DATA_GENETIC_ID(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 6), - SENSITIVE_DATA_BIOMETRIC_ID(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 7), - SENSITIVE_DATA_GEOLOCATION(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 8), - SENSITIVE_DATA_ID_NUMBERS(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 9), - SENSITIVE_DATA_ACCOUNT_INFO(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 10), - SENSITIVE_DATA_UNION_MEMBERSHIP(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 11), - SENSITIVE_DATA_COMMUNICATION_CONTENTS(UspNatV1Field.SENSITIVE_DATA_PROCESSING + 12), - SENSITIVE_DATA_PROCESSING_ALL(UspNatV1Field.SENSITIVE_DATA_PROCESSING + "*"), - CHILD_CONSENTS_FROM_13_TO_16(UspNatV1Field.KNOWN_CHILD_SENSITIVE_DATA_CONSENTS + 1), - CHILD_CONSENTS_BELOW_13(UspNatV1Field.KNOWN_CHILD_SENSITIVE_DATA_CONSENTS + 2), - PERSONAL_DATA_CONSENTS(UspNatV1Field.PERSONAL_DATA_CONSENTS), - MSPA_COVERED_TRANSACTION(UspNatV1Field.MSPA_COVERED_TRANSACTION), - MSPA_OPT_OUT_OPTION_MODE(UspNatV1Field.MSPA_OPT_OUT_OPTION_MODE), - MSPA_SERVICE_PROVIDER_MODE(UspNatV1Field.MSPA_SERVICE_PROVIDER_MODE), - GPC(UspNatV1Field.GPC); + SHARING_NOTICE(UsNatV1Field.SHARING_NOTICE), + SALE_OPT_OUT_NOTICE(UsNatV1Field.SALE_OPT_OUT_NOTICE), + SHARING_OPT_OUT_NOTICE(UsNatV1Field.SHARING_OPT_OUT_NOTICE), + TARGETED_ADVERTISING_OPT_OUT_NOTICE(UsNatV1Field.TARGETED_ADVERTISING_OPT_OUT_NOTICE), + SENSITIVE_DATA_PROCESSING_OPT_OUT_NOTICE(UsNatV1Field.SENSITIVE_DATA_PROCESSING_OPT_OUT_NOTICE), + SENSITIVE_DATA_LIMIT_USE_NOTICE(UsNatV1Field.SENSITIVE_DATA_LIMIT_USE_NOTICE), + SALE_OPT_OUT(UsNatV1Field.SALE_OPT_OUT), + SHARING_OPT_OUT(UsNatV1Field.SHARING_OPT_OUT), + TARGETED_ADVERTISING_OPT_OUT(UsNatV1Field.TARGETED_ADVERTISING_OPT_OUT), + SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 1), + SENSITIVE_DATA_RELIGIOUS_BELIEFS(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 2), + SENSITIVE_DATA_HEALTH_INFO(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 3), + SENSITIVE_DATA_ORIENTATION(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 4), + SENSITIVE_DATA_CITIZENSHIP_STATUS(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 5), + SENSITIVE_DATA_GENETIC_ID(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 6), + SENSITIVE_DATA_BIOMETRIC_ID(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 7), + SENSITIVE_DATA_GEOLOCATION(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 8), + SENSITIVE_DATA_ID_NUMBERS(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 9), + SENSITIVE_DATA_ACCOUNT_INFO(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 10), + SENSITIVE_DATA_UNION_MEMBERSHIP(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 11), + SENSITIVE_DATA_COMMUNICATION_CONTENTS(UsNatV1Field.SENSITIVE_DATA_PROCESSING + 12), + SENSITIVE_DATA_PROCESSING_ALL(UsNatV1Field.SENSITIVE_DATA_PROCESSING + "*"), + CHILD_CONSENTS_FROM_13_TO_16(UsNatV1Field.KNOWN_CHILD_SENSITIVE_DATA_CONSENTS + 1), + CHILD_CONSENTS_BELOW_13(UsNatV1Field.KNOWN_CHILD_SENSITIVE_DATA_CONSENTS + 2), + PERSONAL_DATA_CONSENTS(UsNatV1Field.PERSONAL_DATA_CONSENTS), + MSPA_COVERED_TRANSACTION(UsNatV1Field.MSPA_COVERED_TRANSACTION), + MSPA_OPT_OUT_OPTION_MODE(UsNatV1Field.MSPA_OPT_OUT_OPTION_MODE), + MSPA_SERVICE_PROVIDER_MODE(UsNatV1Field.MSPA_SERVICE_PROVIDER_MODE), + GPC(UsNatV1Field.GPC); @JsonValue final String value diff --git a/src/test/groovy/org/prebid/server/functional/model/db/Account.groovy b/src/test/groovy/org/prebid/server/functional/model/db/Account.groovy index bb089280334..834cb57c114 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/Account.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/Account.groovy @@ -1,12 +1,12 @@ package org.prebid.server.functional.model.db import groovy.transform.ToString -import javax.persistence.Column -import javax.persistence.Convert -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.Table +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table import org.prebid.server.functional.model.AccountStatus import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.typeconverter.AccountConfigTypeConverter @@ -14,7 +14,7 @@ import org.prebid.server.functional.model.db.typeconverter.AccountStatusTypeConv import java.sql.Timestamp -import static javax.persistence.GenerationType.IDENTITY +import static jakarta.persistence.GenerationType.IDENTITY @Entity @Table(name = "accounts_account") diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredImp.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredImp.groovy index 9be9b7a237f..27f66fb03d6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredImp.groovy @@ -1,17 +1,17 @@ package org.prebid.server.functional.model.db import groovy.transform.ToString -import javax.persistence.Column -import javax.persistence.Convert -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.Table +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.ImpConfigTypeConverter import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp -import static javax.persistence.GenerationType.IDENTITY +import static jakarta.persistence.GenerationType.IDENTITY @Entity @Table(name = "stored_imps") diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy index 30a43912297..69264aa04eb 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredRequest.groovy @@ -1,17 +1,17 @@ package org.prebid.server.functional.model.db import groovy.transform.ToString -import javax.persistence.Column -import javax.persistence.Convert -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.Table +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.StoredRequestConfigTypeConverter import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest -import static javax.persistence.GenerationType.IDENTITY +import static jakarta.persistence.GenerationType.IDENTITY @Entity @Table(name = "stored_requests") diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy index 51d59ce0959..b57e793d22e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredResponse.groovy @@ -1,18 +1,18 @@ package org.prebid.server.functional.model.db import groovy.transform.ToString -import javax.persistence.Column -import javax.persistence.Convert -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.Table +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table import org.prebid.server.functional.model.db.typeconverter.StoredAuctionResponseConfigTypeConverter import org.prebid.server.functional.model.db.typeconverter.StoredBidResponseConfigTypeConverter import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.SeatBid -import static javax.persistence.GenerationType.IDENTITY +import static jakarta.persistence.GenerationType.IDENTITY @Entity @Table(name = "stored_responses") diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountConfigTypeConverter.groovy index da0a7e6d1ec..e12009cb569 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountConfigTypeConverter.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.model.db.typeconverter -import javax.persistence.AttributeConverter +import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.util.ObjectMapperWrapper diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountStatusTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountStatusTypeConverter.groovy index 005aae09d1c..b18aa387203 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountStatusTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/AccountStatusTypeConverter.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.model.db.typeconverter -import javax.persistence.AttributeConverter +import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.AccountStatus class AccountStatusTypeConverter implements AttributeConverter { diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ImpConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ImpConfigTypeConverter.groovy index 5a7ac8bb54c..1dd3227b38c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ImpConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ImpConfigTypeConverter.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.model.db.typeconverter -import javax.persistence.AttributeConverter +import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.util.ObjectMapperWrapper diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredAuctionResponseConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredAuctionResponseConfigTypeConverter.groovy index 2f920559c38..e9423f02def 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredAuctionResponseConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredAuctionResponseConfigTypeConverter.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.model.db.typeconverter -import javax.persistence.AttributeConverter +import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.util.ObjectMapperWrapper diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy index 6a1ddb05da8..43120fcad65 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.model.db.typeconverter -import javax.persistence.AttributeConverter +import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.ObjectMapperWrapper diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy index 8668b292d8c..3e968d39565 100644 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.model.db.typeconverter -import javax.persistence.AttributeConverter +import jakarta.persistence.AttributeConverter import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.util.ObjectMapperWrapper diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/Action.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/Action.groovy deleted file mode 100644 index 8760760c00a..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/alert/Action.groovy +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.functional.model.deals.alert - -enum Action { - - RAISE -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertEvent.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertEvent.groovy deleted file mode 100644 index 596b7221c46..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertEvent.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.functional.model.deals.alert - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) -class AlertEvent { - - String id - Action action - AlertPriority priority - ZonedDateTime updatedAt - String name - String details - AlertSource source -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertPriority.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertPriority.groovy deleted file mode 100644 index 502b2d5eb25..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertPriority.groovy +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.functional.model.deals.alert - -enum AlertPriority { - - HIGH, MEDIUM, LOW -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertSource.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertSource.groovy deleted file mode 100644 index 3175711c4be..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertSource.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.functional.model.deals.alert - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) -class AlertSource { - - String env - String dataCenter - String region - String system - String subSystem - String hostId -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/DeliverySchedule.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/DeliverySchedule.groovy deleted file mode 100644 index 8b59632171b..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/DeliverySchedule.groovy +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonFormat -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static java.time.ZoneOffset.UTC -import static org.prebid.server.functional.model.deals.lineitem.LineItem.TIME_PATTERN - -@ToString(includeNames = true, ignoreNulls = true) -class DeliverySchedule { - - String planId - - @JsonFormat(pattern = TIME_PATTERN) - ZonedDateTime startTimeStamp - - @JsonFormat(pattern = TIME_PATTERN) - ZonedDateTime endTimeStamp - - @JsonFormat(pattern = TIME_PATTERN) - ZonedDateTime updatedTimeStamp - - Set tokens - - static getDefaultDeliverySchedule() { - new DeliverySchedule(planId: PBSUtils.randomString, - startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), - endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusDays(1), - updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), - tokens: [Token.defaultToken] - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/FrequencyCap.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/FrequencyCap.groovy deleted file mode 100644 index 995ae1b9309..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/FrequencyCap.groovy +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -import static PeriodType.DAY - -@ToString(includeNames = true, ignoreNulls = true) -class FrequencyCap { - - String fcapId - Integer count - Integer periods - String periodType - - static getDefaultFrequencyCap() { - new FrequencyCap(count: 1, - fcapId: PBSUtils.randomString, - periods: 1, - periodType: DAY - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItem.groovy deleted file mode 100644 index 43051f0ab50..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItem.groovy +++ /dev/null @@ -1,74 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonFormat -import groovy.transform.ToString -import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting -import org.prebid.server.functional.util.PBSUtils - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static LineItemStatus.ACTIVE -import static java.time.ZoneOffset.UTC -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.VERY_HIGH - -@ToString(includeNames = true, ignoreNulls = true) -class LineItem { - - public static final String TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'" - - String lineItemId - - String extLineItemId - - String dealId - - List sizes - - String accountId - - String source - - Price price - - RelativePriority relativePriority - - @JsonFormat(pattern = TIME_PATTERN) - ZonedDateTime startTimeStamp - - @JsonFormat(pattern = TIME_PATTERN) - ZonedDateTime endTimeStamp - - @JsonFormat(pattern = TIME_PATTERN) - ZonedDateTime updatedTimeStamp - - LineItemStatus status - - List frequencyCaps - - List deliverySchedules - - Targeting targeting - - static LineItem getDefaultLineItem(String accountId) { - int plannerAdapterLineItemId = PBSUtils.randomNumber - String plannerAdapterName = PBSUtils.randomString - new LineItem(lineItemId: "${plannerAdapterName}-$plannerAdapterLineItemId", - extLineItemId: plannerAdapterLineItemId, - dealId: PBSUtils.randomString, - sizes: [LineItemSize.defaultLineItemSize], - accountId: accountId, - source: GENERIC.name().toLowerCase(), - price: Price.defaultPrice, - relativePriority: VERY_HIGH, - startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), - endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusMonths(1), - updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), - status: ACTIVE, - frequencyCaps: [], - deliverySchedules: [DeliverySchedule.defaultDeliverySchedule], - targeting: Targeting.defaultTargeting - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemSize.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemSize.groovy deleted file mode 100644 index 2ff7af18dec..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemSize.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import groovy.transform.ToString - -@ToString(includeNames = true) -class LineItemSize { - - Integer w - Integer h - - static getDefaultLineItemSize() { - new LineItemSize(w: 300, - h: 250 - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemStatus.groovy deleted file mode 100644 index c2dded2d3d2..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemStatus.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonValue - -enum LineItemStatus { - - ACTIVE("active"), - DELETED("deleted"), - PAUSED("paused") - - @JsonValue - final String value - - private LineItemStatus(String value) { - this.value = value - } - - @Override - String toString() { - value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/MediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/MediaType.groovy deleted file mode 100644 index 949da3d9b53..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/MediaType.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonValue - -enum MediaType { - - BANNER("banner") - - @JsonValue - final String value - - private MediaType(String value) { - this.value = value - } - - @Override - String toString() { - value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/PeriodType.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/PeriodType.groovy deleted file mode 100644 index 10ca6f59d9c..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/PeriodType.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonValue - -enum PeriodType { - - HOUR("hour"), - DAY("day"), - WEEK("week"), - MONTH("month"), - CAMPAIGN("campaign") - - @JsonValue - final String value - - private PeriodType(String value) { - this.value = value - } - - @Override - String toString() { - value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Price.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Price.groovy deleted file mode 100644 index 5c0bb616ed6..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Price.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -@ToString(includeNames = true, ignoreNulls = true) -class Price { - - BigDecimal cpm - String currency - - static getDefaultPrice() { - new Price(cpm: PBSUtils.randomPrice, currency: "USD") - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/RelativePriority.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/RelativePriority.groovy deleted file mode 100644 index 911da0b365f..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/RelativePriority.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonValue - -enum RelativePriority { - - VERY_HIGH(1), - HIGH(2), - MEDIUM(3), - LOW(4), - VERY_LOW(5) - - @JsonValue - final Integer value - - private RelativePriority(Integer value) { - this.value = value - } - - @Override - String toString() { - value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Token.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Token.groovy deleted file mode 100644 index e7dd3f2fc5d..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Token.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem - -import com.fasterxml.jackson.annotation.JsonProperty -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class Token { - - @JsonProperty("class") - Integer priorityClass - - Integer total - - static getDefaultToken() { - new Token(priorityClass: 1, - total: 1000 - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/BooleanOperator.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/BooleanOperator.groovy deleted file mode 100644 index 0e4a4740e53..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/BooleanOperator.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem.targeting - -import com.fasterxml.jackson.annotation.JsonValue - -enum BooleanOperator { - - AND('$and'), - OR('$or'), - NOT('$not'), - - INVALID('$invalid'), - UPPERCASE_AND('$AND') - - @JsonValue - final String value - - private BooleanOperator(String value) { - this.value = value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunction.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunction.groovy deleted file mode 100644 index 54a1353808e..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunction.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem.targeting - -import com.fasterxml.jackson.annotation.JsonValue - -enum MatchingFunction { - - MATCHES('$matches'), - IN('$in'), - INTERSECTS('$intersects'), - WITHIN('$within') - - @JsonValue - final String value - - private MatchingFunction(String value) { - this.value = value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunctionNode.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunctionNode.groovy deleted file mode 100644 index 6e639fe4383..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunctionNode.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem.targeting - -import com.fasterxml.jackson.annotation.JsonValue -import groovy.transform.PackageScope - -@PackageScope -class MatchingFunctionNode { - - Map> matchingFunctionMultipleValuesNode - - Map matchingFunctionSingleValueNode - - @JsonValue - def getMatchingFunctionNode() { - matchingFunctionMultipleValuesNode ?: matchingFunctionSingleValueNode - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/Targeting.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/Targeting.groovy deleted file mode 100644 index 12c9633cbaf..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/Targeting.groovy +++ /dev/null @@ -1,92 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem.targeting - -import com.fasterxml.jackson.annotation.JsonValue -import org.prebid.server.functional.model.deals.lineitem.LineItemSize - -import static BooleanOperator.AND -import static MatchingFunction.INTERSECTS -import static TargetingType.AD_UNIT_MEDIA_TYPE -import static TargetingType.AD_UNIT_SIZE -import static org.prebid.server.functional.model.deals.lineitem.MediaType.BANNER -import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.NOT -import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.OR - -class Targeting { - - private final Map> rootNode - - private final Map singleTargetingRootNode - - @JsonValue - def getSerializableRootNode() { - rootNode ?: singleTargetingRootNode - } - - private Targeting(Builder builder) { - rootNode = [(builder.rootOperator): builder.targetingNodes] - } - - private Targeting(Builder builder, TargetingNode targetingNode) { - singleTargetingRootNode = [(builder.rootOperator): targetingNode] - } - - Map> getTargetingRootNode() { - rootNode.asImmutable() - } - - static Targeting getDefaultTargeting() { - defaultTargetingBuilder.build() - } - - static Builder getDefaultTargetingBuilder() { - new Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) - } - - static Targeting getInvalidTwoRootNodesTargeting() { - defaultTargeting.tap { rootNode.put(OR, []) } - } - - static class Builder { - - private BooleanOperator rootOperator - private List targetingNodes = [] - - Builder(BooleanOperator rootOperator = AND) { - this.rootOperator = rootOperator - } - - Builder addTargeting(TargetingType targetingType, - MatchingFunction matchingFunction, - List targetingValues) { - MatchingFunctionNode matchingFunctionNode = new MatchingFunctionNode(matchingFunctionMultipleValuesNode: [(matchingFunction): targetingValues]) - addTargetingNode(targetingType, matchingFunctionNode) - this - } - - Builder addTargeting(TargetingType targetingType, - MatchingFunction matchingFunction, - Object targetingValue) { - MatchingFunctionNode matchingFunctionNode = new MatchingFunctionNode(matchingFunctionSingleValueNode: [(matchingFunction): targetingValue]) - addTargetingNode(targetingType, matchingFunctionNode) - this - } - - private void addTargetingNode(TargetingType targetingType, - MatchingFunctionNode matchingFunctionNode) { - targetingNodes << new TargetingNode([(targetingType): matchingFunctionNode]) - } - - Targeting build() { - new Targeting(this) - } - - Targeting buildNotBooleanOperatorTargeting(TargetingType targetingType, - MatchingFunction matchingFunction, - List targetingValues) { - rootOperator = NOT - MatchingFunctionNode matchingFunctionNode = new MatchingFunctionNode(matchingFunctionSingleValueNode: [(matchingFunction): targetingValues]) - new Targeting(this, new TargetingNode([(targetingType): matchingFunctionNode])) - } - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingNode.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingNode.groovy deleted file mode 100644 index 4e8ccecd318..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingNode.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem.targeting - -import com.fasterxml.jackson.annotation.JsonValue -import groovy.transform.PackageScope -import groovy.transform.TupleConstructor - -@PackageScope -@TupleConstructor -class TargetingNode { - - @JsonValue - Map targetingNode -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingType.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingType.groovy deleted file mode 100644 index 5434d38a59a..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingType.groovy +++ /dev/null @@ -1,46 +0,0 @@ -package org.prebid.server.functional.model.deals.lineitem.targeting - -import com.fasterxml.jackson.annotation.JsonValue - -enum TargetingType { - - AD_UNIT_SIZE("adunit.size"), - AD_UNIT_MEDIA_TYPE("adunit.mediatype"), - AD_UNIT_AD_SLOT("adunit.adslot"), - SITE_DOMAIN("site.domain"), - SITE_PUBLISHER_DOMAIN("site.publisher.domain"), - REFERRER("site.referrer"), - APP_BUNDLE("app.bundle"), - DEVICE_COUNTRY("device.geo.ext.geoprovider.country"), - DEVICE_TYPE("device.ext.deviceinfoprovider.type"), - DEVICE_OS("device.ext.deviceinfoprovider.osfamily"), - DEVICE_REGION("device.geo.ext.geoprovider.region"), - DEVICE_METRO("device.geo.ext.geoprovider.metro"), - PAGE_POSITION("pos"), - LOCATION("geo.distance"), - BIDP("bidp."), - BIDP_ACCOUNT_ID(BIDP.value + "rubicon.accountId"), - USER_SEGMENT("segment."), - USER_SEGMENT_NAME(USER_SEGMENT.value + "name"), - UFPD("ufpd."), - UFPD_KEYWORDS(UFPD.value + "keywords"), - UFPD_BUYER_UID(UFPD.value + "buyeruid"), - UFPD_BUYER_UIDS(UFPD.value + "buyeruids"), - UFPD_YOB(UFPD.value + "yob"), - SFPD("sfpd."), - SFPD_AMP(SFPD.value + "amp"), - SFPD_LANGUAGE(SFPD.value + "language"), - SFPD_KEYWORDS(SFPD.value + "keywords"), - SFPD_BUYER_ID(SFPD.value + "buyerid"), - SFPD_BUYER_IDS(SFPD.value + "buyerids"), - DOW("user.ext.time.userdow"), - HOUR("user.ext.time.userhour"), - INVALID("invalid.targeting.type") - - @JsonValue - final String value - - private TargetingType(String value) { - this.value = value - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/register/CurrencyServiceState.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/register/CurrencyServiceState.groovy deleted file mode 100644 index 4c0db24acd3..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/register/CurrencyServiceState.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.functional.model.deals.register - -import groovy.transform.ToString - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -class CurrencyServiceState { - - ZonedDateTime lastUpdate -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/register/RegisterRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/register/RegisterRequest.groovy deleted file mode 100644 index 75daf33adc3..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/register/RegisterRequest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.functional.model.deals.register - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class RegisterRequest { - - BigDecimal healthIndex - Status status - String hostInstanceId - String region - String vendor -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/register/Status.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/register/Status.groovy deleted file mode 100644 index ad065e9ac4a..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/register/Status.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.functional.model.deals.register - -import groovy.transform.ToString -import org.prebid.server.functional.model.deals.report.DeliveryStatisticsReport - -@ToString(includeNames = true, ignoreNulls = true) -class Status { - - CurrencyServiceState currencyRates - DeliveryStatisticsReport dealsStatus -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliverySchedule.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliverySchedule.groovy deleted file mode 100644 index 5701757e569..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliverySchedule.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import groovy.transform.ToString - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -class DeliverySchedule { - - String planId - ZonedDateTime planStartTimeStamp - ZonedDateTime planExpirationTimeStamp - ZonedDateTime planUpdatedTimeStamp - Set tokens -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliveryStatisticsReport.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliveryStatisticsReport.groovy deleted file mode 100644 index 5ee7340c3dd..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliveryStatisticsReport.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import groovy.transform.ToString - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -class DeliveryStatisticsReport { - - String reportId - String instanceId - String vendor - String region - Long clientAuctions - Set lineItemStatus - ZonedDateTime reportTimeStamp - ZonedDateTime dataWindowStartTimeStamp - ZonedDateTime dataWindowEndTimeStamp -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/Event.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/Event.groovy deleted file mode 100644 index 495bfaf673e..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/Event.groovy +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class Event { - - String type - Long count -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatus.groovy deleted file mode 100644 index a68029852c6..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatus.groovy +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class LineItemStatus { - - String lineItemSource - String lineItemId - String dealId - String extLineItemId - Long accountAuctions - Long domainMatched - Long targetMatched - Long targetMatchedButFcapped - Long targetMatchedButFcapLookupFailed - Long pacingDeferred - Long sentToBidder - Long sentToBidderAsTopMatch - Long receivedFromBidder - Long receivedFromBidderInvalidated - Long sentToClient - Long sentToClientAsTopMatch - Set lostToLineItems - Set events - Set deliverySchedule - String readyAt - Long spentTokens - Long pacingFrequency -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatusReport.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatusReport.groovy deleted file mode 100644 index 6fb1766c534..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatusReport.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import groovy.transform.ToString - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -class LineItemStatusReport { - - String lineItemId - DeliverySchedule deliverySchedule - Long spentTokens - ZonedDateTime readyToServeTimestamp - Long pacingFrequency - String accountId - Map target -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/LostToLineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/LostToLineItem.groovy deleted file mode 100644 index 58567ffc974..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/LostToLineItem.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class LostToLineItem { - - String lineItemSource - String lineItemId - Long count -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/Token.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/Token.groovy deleted file mode 100644 index 89310a5d73e..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/report/Token.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.functional.model.deals.report - -import com.fasterxml.jackson.annotation.JsonProperty -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class Token { - - @JsonProperty("class") - Integer priorityClass - - Integer total - - Long spent - - Long totalSpent -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/Segment.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/Segment.groovy deleted file mode 100644 index f11d8f32ff1..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/Segment.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -@ToString(includeNames = true, ignoreNulls = true) -class Segment { - - String id - - static getDefaultSegment() { - new Segment(id: PBSUtils.randomString) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/User.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/User.groovy deleted file mode 100644 index 6d2e527a0e6..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/User.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class User { - - List data - UserExt ext - - static getDefaultUser() { - new User(data: [UserData.defaultUserData], - ext: UserExt.defaultUserExt - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserData.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserData.groovy deleted file mode 100644 index 5687d89285e..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserData.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -@ToString(includeNames = true, ignoreNulls = true) -class UserData { - - String id - String name - List segment - - static UserData getDefaultUserData() { - new UserData(id: PBSUtils.randomString, - name: PBSUtils.randomString, - segment: [Segment.defaultSegment] - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetails.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetails.groovy deleted file mode 100644 index 780ec674b24..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetails.groovy +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class UserDetails { - - List userData - List fcapIds -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsRequest.groovy deleted file mode 100644 index 74985c5808f..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsRequest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -class UserDetailsRequest { - - ZonedDateTime time - List ids -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsResponse.groovy deleted file mode 100644 index 143aee234d9..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsResponse.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString -import org.prebid.server.functional.model.ResponseModel - -@ToString(includeNames = true, ignoreNulls = true) -class UserDetailsResponse implements ResponseModel { - - User user - - static UserDetailsResponse getDefaultUserResponse(User user = User.defaultUser) { - new UserDetailsResponse(user: user) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserExt.groovy deleted file mode 100644 index 53a89aa97e2..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserExt.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString -import org.prebid.server.functional.util.PBSUtils - -@ToString(includeNames = true, ignoreNulls = true) -class UserExt { - - List fcapIds - - static getDefaultUserExt() { - new UserExt(fcapIds: [PBSUtils.randomString]) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserId.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserId.groovy deleted file mode 100644 index 8edeac5336d..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserId.groovy +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class UserId { - - String type - String id -} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/WinEventNotification.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/WinEventNotification.groovy deleted file mode 100644 index bea8f7c296b..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/WinEventNotification.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.functional.model.deals.userdata - -import groovy.transform.ToString -import org.prebid.server.functional.model.deals.lineitem.FrequencyCap - -import java.time.ZonedDateTime - -@ToString(includeNames = true, ignoreNulls = true) -class WinEventNotification { - - String bidderCode - String bidId - String lineItemId - String region - List userIds - ZonedDateTime winEventDateTime - ZonedDateTime lineUpdatedDateTime - List frequencyCaps -} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/generalplanner/PlansResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/generalplanner/PlansResponse.groovy deleted file mode 100644 index f9a9d4548e7..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/generalplanner/PlansResponse.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.functional.model.mock.services.generalplanner - -import com.fasterxml.jackson.annotation.JsonValue -import org.prebid.server.functional.model.ResponseModel -import org.prebid.server.functional.model.deals.lineitem.LineItem - -class PlansResponse implements ResponseModel { - - List lineItems - - static PlansResponse getDefaultPlansResponse(String accountId) { - new PlansResponse(lineItems: [LineItem.getDefaultLineItem(accountId)]) - } - - @JsonValue - List getLineItems() { - lineItems - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/GvlSpecificationVersion.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/GvlSpecificationVersion.groovy new file mode 100644 index 00000000000..bc698d7bd52 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/GvlSpecificationVersion.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.mock.services.vendorlist + +import com.fasterxml.jackson.annotation.JsonValue + +enum GvlSpecificationVersion { + + V2(2), V3(3) + + @JsonValue + private final Integer value + + GvlSpecificationVersion(Integer value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy index 7d61d88ff41..d44755978e4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/mock/services/vendorlist/VendorListResponse.groovy @@ -1,15 +1,15 @@ package org.prebid.server.functional.model.mock.services.vendorlist import org.prebid.server.functional.util.PBSUtils - import java.time.Clock import java.time.ZonedDateTime +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V2 import static org.prebid.server.functional.util.privacy.TcfConsent.VENDOR_LIST_VERSION class VendorListResponse { - Integer gvlSpecificationVersion + GvlSpecificationVersion gvlSpecificationVersion Integer vendorListVersion Integer tcfPolicyVersion ZonedDateTime lastUpdated @@ -17,7 +17,7 @@ class VendorListResponse { static VendorListResponse getDefaultVendorListResponse() { new VendorListResponse().tap { - it.gvlSpecificationVersion = 2 + it.gvlSpecificationVersion = V2 it.vendorListVersion = VENDOR_LIST_VERSION it.lastUpdated = ZonedDateTime.now(Clock.systemUTC()).minusWeeks(2) } diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy index b1d4368cf75..38827a00a76 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy @@ -5,23 +5,27 @@ import org.prebid.server.functional.util.privacy.model.State enum Country { - USA("USA"), - CAN("CAN"), - MULTIPLE("*") + USA("USA","US"), + CAN("CAN","CA"), + BULGARIA("BGR","BG"), + MULTIPLE("*","*") @JsonValue - final String value + final String ISOAlpha3 - Country(String value) { - this.value = value + final String ISOAlpha2 + + Country(String ISOAlpha3,String ISOAlpha2) { + this.ISOAlpha3 = ISOAlpha3 + this.ISOAlpha2 = ISOAlpha2 } @Override String toString() { - value + ISOAlpha3 } String withState(State state) { - return "${value}.${state.abbreviation}".toString() + return "${ISOAlpha3}.${state.abbreviation}".toString() } } diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy index d8770197821..24b57f57bba 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.util.PBSUtils @EqualsAndHashCode @@ -18,6 +19,7 @@ class ModelGroup { Map values @JsonProperty("default") BigDecimal defaultFloor + List noFloorSignalBidders static ModelGroup getModelGroup() { new ModelGroup( diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy index b9af0cd1dc7..431890c371d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy @@ -4,6 +4,7 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.ResponseModel +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.USD @@ -18,6 +19,7 @@ class PriceFloorData implements ResponseModel { String floorsSchemaVersion Integer modelTimestamp List modelGroups + List noFloorSignalBidders static PriceFloorData getPriceFloorData() { new PriceFloorData(floorProvider: PBSUtils.randomString, diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorField.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorField.groovy index 1858efaa47f..d506e1ab512 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorField.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorField.groovy @@ -17,6 +17,7 @@ enum PriceFloorField { AD_UNIT_CODE("adUnitCode"), COUNTRY("country"), DEVICE_TYPE("deviceType"), + BIDDER("bidder"), BOGUS("bogus") @JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/Rule.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/Rule.groovy index dd3a58631fb..21415db1447 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/Rule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/Rule.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.model.pricefloors import com.fasterxml.jackson.annotation.JsonValue import org.apache.commons.lang3.StringUtils +import org.prebid.server.functional.model.bidder.BidderName import java.lang.reflect.Modifier @@ -21,6 +22,7 @@ class Rule { private String adUnitCode private Country country private DeviceType deviceType + private BidderName bidder // TODO add factory for delimiter @JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/EnforcementRequirement.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/EnforcementRequirement.groovy index 2022d4f754e..23e2684e51a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/privacy/EnforcementRequirement.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/EnforcementRequirement.groovy @@ -1,5 +1,8 @@ package org.prebid.server.functional.model.privacy +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.Purpose @@ -7,11 +10,16 @@ import org.prebid.server.functional.model.config.PurposeEnforcement import org.prebid.server.functional.util.privacy.TcfConsent @ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class EnforcementRequirement { Purpose purpose PurposeEnforcement enforcePurpose + @JsonProperty("enforce_purpose") + PurposeEnforcement enforcePurposeSnakeCase Boolean enforceVendor + @JsonProperty("enforce_vendor") + Boolean enforceVendorSnakeCase Integer vendorConsentBitField Integer vendorLegitimateInterestBitField List vendorExceptions diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy new file mode 100644 index 00000000000..490c1456e53 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy @@ -0,0 +1,53 @@ +package org.prebid.server.functional.model.privacy + +import org.prebid.server.functional.model.request.auction.ActivityType +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.cookiesync.CookieSyncRequest +import org.prebid.server.functional.model.request.setuid.SetuidRequest + +enum Metric { + + ALERT_GENERAL("alerts.general"), + PROCESSED_ACTIVITY_RULES_COUNT("requests.activity.processedrules.count"), + ACCOUNT_PROCESSED_RULES_COUNT("requests.activity.processedrules.count"), + TEMPLATE_ADAPTER_DISALLOWED_COUNT("adapter.{bidderName}.activity.{activityType}.disallowed.count"), + TEMPLATE_ACCOUNT_DISALLOWED_COUNT("account.{accountId}.activity.{activityType}.disallowed.count"), + TEMPLATE_REQUEST_DISALLOWED_COUNT("requests.activity.{activityType}.disallowed.count"), + + final String value + + Metric(String value) { + this.value = value + } + + String getValue(BidRequest bidRequest, String accountId, ActivityType activityType) { + if (bidRequest.imp.size() != 1) { + throw new IllegalStateException("No imp found") + } + replaceValues(bidRequest.imp.first.bidderName.value, accountId, activityType.metricValue) + } + + String getValue(BidRequest bidRequest, ActivityType activityType) { + if (bidRequest.imp.size() != 1) { + throw new IllegalStateException("No imp found") + } + replaceValues(bidRequest.imp.first.bidderName.value, bidRequest.accountId, activityType.metricValue) + } + + String getValue(CookieSyncRequest syncRequest, ActivityType activityType) { + if (syncRequest.bidders.size() != 1) { + throw new IllegalStateException("No bidder found") + } + replaceValues(syncRequest.bidders.first.value, syncRequest.account, activityType.metricValue) + } + + String getValue(SetuidRequest syncRequest, ActivityType activityType) { + replaceValues(syncRequest.bidder.value, syncRequest.account, activityType.metricValue) + } + + private String replaceValues(String bidderName, String accountId, String activityType) { + this.value.replaceAll('\\{bidderName}', bidderName) + .replaceAll('\\{accountId}', accountId) + .replaceAll('\\{activityType}', activityType) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/GppSectionId.groovy b/src/test/groovy/org/prebid/server/functional/model/request/GppSectionId.groovy index d71d60d8aa5..f3866eb24cd 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/GppSectionId.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/GppSectionId.groovy @@ -4,13 +4,13 @@ import com.fasterxml.jackson.annotation.JsonValue import com.iab.gpp.encoder.section.HeaderV1 import com.iab.gpp.encoder.section.TcfCaV1 import com.iab.gpp.encoder.section.TcfEuV2 -import com.iab.gpp.encoder.section.UspCaV1 -import com.iab.gpp.encoder.section.UspCoV1 -import com.iab.gpp.encoder.section.UspCtV1 -import com.iab.gpp.encoder.section.UspNatV1 -import com.iab.gpp.encoder.section.UspUtV1 +import com.iab.gpp.encoder.section.UsCaV1 +import com.iab.gpp.encoder.section.UsCoV1 +import com.iab.gpp.encoder.section.UsCtV1 +import com.iab.gpp.encoder.section.UsNatV1 +import com.iab.gpp.encoder.section.UsUtV1 +import com.iab.gpp.encoder.section.UsVaV1 import com.iab.gpp.encoder.section.UspV1 -import com.iab.gpp.encoder.section.UspVaV1 enum GppSectionId { @@ -18,12 +18,12 @@ enum GppSectionId { HEADER_V1(HeaderV1.ID), TCF_CA_V1(TcfCaV1.ID), USP_V1(UspV1.ID), - USP_NAT_V1(UspNatV1.ID), - USP_CA_V1(UspCaV1.ID), - USP_VA_V1(UspVaV1.ID), - USP_CO_V1(UspCoV1.ID), - USP_UT_V1(UspUtV1.ID), - USP_CT_V1(UspCtV1.ID) + US_NAT_V1(UsNatV1.ID), + US_CA_V1(UsCaV1.ID), + US_VA_V1(UsVaV1.ID), + US_CO_V1(UsCoV1.ID), + US_UT_V1(UsUtV1.ID), + US_CT_V1(UsCtV1.ID) @JsonValue final Integer value diff --git a/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy index 40f9115b484..fffb2c86e05 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy @@ -27,6 +27,8 @@ class AmpRequest { Boolean gdprApplies String addtlConsent String gppSid + String unknownField + Integer secondUnknownField static AmpRequest getDefaultAmpRequest() { def request = new AmpRequest() diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ActivityType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ActivityType.groovy index ef76b4023fb..8b7e318680d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ActivityType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ActivityType.groovy @@ -1,6 +1,8 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.util.Case +import org.prebid.server.functional.util.PBSUtils enum ActivityType { @@ -13,7 +15,6 @@ enum ActivityType { TRANSMIT_TID("transmitTid"), TRANSMIT_EIDS("transmitEids") - @JsonValue final String value ActivityType(String value) { @@ -23,4 +24,15 @@ enum ActivityType { String getMetricValue() { name().toLowerCase() } + + @JsonValue + String getValue() { + def type = PBSUtils.getRandomEnum(Case.class) + if (type == Case.KEBAB) { + PBSUtils.convertCase(value, Case.KEBAB) + } else if (type == Case.SNAKE) { + PBSUtils.convertCase(value, Case.SNAKE) + } + return value + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AllowActivities.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AllowActivities.groovy index aeafaf30fa6..4ce12b57477 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/AllowActivities.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AllowActivities.groovy @@ -1,15 +1,16 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString +import static org.prebid.server.functional.model.request.auction.ActivityType.ENRICH_UFPD +import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.REPORT_ANALYTICS +import static org.prebid.server.functional.model.request.auction.ActivityType.SYNC_USER import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_TID import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD -import static org.prebid.server.functional.model.request.auction.ActivityType.REPORT_ANALYTICS -import static org.prebid.server.functional.model.request.auction.ActivityType.ENRICH_UFPD -import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS -import static org.prebid.server.functional.model.request.auction.ActivityType.SYNC_USER @ToString(includeNames = true, ignoreNulls = true) class AllowActivities { @@ -23,6 +24,47 @@ class AllowActivities { Activity transmitPreciseGeo Activity transmitTid + //Different case for each activity + @JsonProperty("sync-user") + Activity syncUserKebabCase + @JsonProperty("sync_user") + Activity syncUserSnakeCase + + @JsonProperty("fetch-bids") + Activity fetchBidsKebabCase + @JsonProperty("fetch_bids") + Activity fetchBidsSnakeCase + + @JsonProperty("enrich-ufpd") + Activity enrichUfpdKebabCase + @JsonProperty("enrich_ufpd") + Activity enrichUfpdSnakeCase + + @JsonProperty("report-analytics") + Activity reportAnalyticsKebabCase + @JsonProperty("report_analytics") + Activity reportAnalyticsSnakeCase + + @JsonProperty("transmit-ufpd") + Activity transmitUfpdKebabCase + @JsonProperty("transmit_ufpd") + Activity transmitUfpdSnakeCase + + @JsonProperty("transmit-eids") + Activity transmitEidsKebabCase + @JsonProperty("transmit_eids") + Activity transmitEidsSnakeCase + + @JsonProperty("transmit-precise-geo") + Activity transmitPreciseGeoKebabCase + @JsonProperty("transmit_precise_geo") + Activity transmitPreciseGeoSnakeCase + + @JsonProperty("transmit-tid") + Activity transmitTidKebabCase + @JsonProperty("transmit_tid") + Activity transmitTidSnakeCase + static AllowActivities getDefaultAllowActivities(ActivityType activityType, Activity activity) { new AllowActivities().tap { switch (activityType) { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AnalyticsOptions.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AnalyticsOptions.groovy new file mode 100644 index 00000000000..7ca046c1f95 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AnalyticsOptions.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class AnalyticsOptions { + + Boolean enableClientDetails +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AnyUnsupportedBidder.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AnyUnsupportedBidder.groovy new file mode 100644 index 00000000000..53858b3bdd0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AnyUnsupportedBidder.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.EqualsAndHashCode + +@EqualsAndHashCode +class AnyUnsupportedBidder { + + String anyUnsupportedField +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Asset.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Asset.groovy index ff468288f18..4ba1a6bc36f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Asset.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Asset.groovy @@ -25,11 +25,11 @@ class Asset { } } - static Asset getImgAsset() { + static Asset getImgAsset(String url = PBSUtils.randomString) { new Asset().tap { id = 2 required = 1 - img = new AssetImage(type: 3, w: PBSUtils.randomNumber, h: PBSUtils.randomNumber) + img = new AssetImage(type: 3, w: PBSUtils.randomNumber, h: PBSUtils.randomNumber, url: url) } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy index 9fd950aaf21..57d9bbd40ee 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Audio.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Audio { List mimes diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy index b9d4faf3e16..b4d6c23f4f5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Banner.groovy @@ -1,13 +1,16 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class Banner { List format - Integer w - Integer h + @JsonProperty("w") + Integer weight + @JsonProperty("h") + Integer height List btype List battr Integer pos diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy index bcc040e7946..c253291dbf8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy @@ -9,4 +9,7 @@ class BidRequestExt { Prebid prebid SupplyChain schain AppNexus appnexus + String bc + String platform + IxDiag ixdiag } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy index 9b5c78f5d97..a1078731f44 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy @@ -19,6 +19,7 @@ class Bidder { @JsonProperty("appnexus") AppNexus appNexus Openx openx + Ix ix static Bidder getDefaultBidder() { new Bidder().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy index dc01fb12b3a..ee029b56c5b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy @@ -1,9 +1,12 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class BidderControls { GenericPreferredBidder generic + @JsonProperty("GeNeRiC") + GenericPreferredBidder genericAnyCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Condition.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Condition.groovy index c08f5983f8e..8a1f131ef04 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Condition.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Condition.groovy @@ -1,15 +1,27 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString import org.prebid.server.functional.model.bidder.BidderName -import org.prebid.server.functional.model.request.GppSectionId @ToString(includeNames = true, ignoreNulls = true) class Condition { List componentType + @JsonProperty("component_type") + List componentTypeSnakeCase + @JsonProperty("component-type") + List componentTypeKebabCase List componentName + @JsonProperty("component_name") + List componentNameSnakeCase + @JsonProperty("component-name") + List componentNameKebabCase List gppSid + @JsonProperty("gpp_sid") + List gppSidSnakeCase + @JsonProperty("gpp-sid") + List gppSidKebabCase List geo String gpc diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Content.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Content.groovy index 9634910fb02..7b49458a5fc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Content.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Content.groovy @@ -1,9 +1,11 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Content { String id diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy index ccd5ff2fb11..894ba61e4cf 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Data.groovy @@ -1,7 +1,7 @@ package org.prebid.server.functional.model.request.auction -import groovy.transform.ToString import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy index 9895f860b40..6dbe31760f4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy @@ -2,8 +2,11 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class Deal { @@ -15,4 +18,8 @@ class Deal { List wseat List wadomain DealExt ext -} + + static Deal getDefaultDeal() { + new Deal(id: PBSUtils.randomString) + } + } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy index 90b57aa8fb9..d59c145551a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy @@ -1,7 +1,9 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true) class DealExt { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy index 4ab7823193c..42b54c6522a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy @@ -2,8 +2,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class DealLineItem { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Dooh.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Dooh.groovy index b5cefe73339..321d7e53585 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Dooh.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Dooh.groovy @@ -2,13 +2,15 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode class Dooh { - + String id String name List venueType diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DoohExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DoohExt.groovy index d7a2bcee7b8..1654b806d5c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DoohExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DoohExt.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class DoohExt { DoohExtData data diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy index ed0beafdc31..18169329cf5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Dsa.groovy @@ -6,6 +6,8 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.request.auction.DsaPubRender.PUB_MIGHT_RENDER + @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @@ -18,7 +20,7 @@ class Dsa { static Dsa getDefaultDsa(DsaRequired dsaRequired = PBSUtils.getRandomEnum(DsaRequired)) { new Dsa(dsaRequired: dsaRequired, - pubRender: PBSUtils.getRandomEnum(DsaPubRender), + pubRender: PUB_MIGHT_RENDER, dataToPub: PBSUtils.getRandomEnum(DsaDataToPub), transparency: [DsaTransparency.defaultDsaTransparency]) } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy index 1b70bd08799..2cb344f6967 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @@ -10,11 +11,18 @@ class Eid { String source List uids + String inserter + String matcher + @JsonProperty("mm") + Integer matchMethod static Eid getDefaultEid(String source = PBSUtils.randomString) { new Eid().tap { it.source = source it.uids = [Uid.defaultUid] + it.inserter = PBSUtils.randomString + it.matcher = PBSUtils.randomString + it.matchMethod = PBSUtils.randomNumber } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidPriceFloorEnforcement.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidPriceFloorEnforcement.groovy index 1aab3cc8493..7cea0e622e1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidPriceFloorEnforcement.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidPriceFloorEnforcement.groovy @@ -1,10 +1,12 @@ package org.prebid.server.functional.model.request.auction import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.pricefloors.PriceFloorEnforcement @ToString(includeNames = true, ignoreNulls = true) class ExtPrebidPriceFloorEnforcement extends PriceFloorEnforcement { Integer enforceRate + List noFloorSignalBidders } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy index de56f5919c0..c669d61f5a0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy @@ -6,10 +6,10 @@ import groovy.transform.ToString @ToString enum FetchStatus { - NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR + NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK @JsonValue String getValue() { - name().toLowerCase() + name().toLowerCase().replace('_', '-') } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy index 0f8236481f5..f6d1798ca57 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy @@ -1,20 +1,28 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class Format { - Integer w - Integer h - Integer wratio - Integer hratio - Integer wmin + @JsonProperty("w") + Integer weight + @JsonProperty("h") + Integer height + @JsonProperty("wratio") + Integer weightRatio + @JsonProperty("hratio") + Integer heightRatio + @JsonProperty("wmin") + Integer weightMin static Format getDefaultFormat() { new Format().tap { - w = 300 - h = 250 + weight = 300 + height = 250 } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/GeoExtGeoProvider.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/GeoExtGeoProvider.groovy index c2c15f595da..1aa51084f20 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/GeoExtGeoProvider.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/GeoExtGeoProvider.groovy @@ -1,5 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) class GeoExtGeoProvider { String country diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index 9b324e07e9c..dbea9b32624 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -1,11 +1,13 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.response.auction.MediaType import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO @@ -33,7 +35,7 @@ class Imp { BigDecimal bidFloor Currency bidFloorCur Integer clickBrowser - Integer secure + SecurityLevel secure List iframeBuster Integer rwdd Integer ssai @@ -74,4 +76,38 @@ class Imp { ext = ImpExt.defaultImpExt } } + + @JsonIgnore + BidderName getBidderName() { + def bidder = ext?.prebid?.bidder + if (!bidder) { + throw new IllegalStateException("No bidder found") + } + + def bidderNames = [ + (bidder.alias) : BidderName.ALIAS, + (bidder.generic) : BidderName.GENERIC, + (bidder.genericCamelCase): BidderName.GENERIC_CAMEL_CASE, + (bidder.rubicon) : BidderName.RUBICON, + (bidder.appNexus) : BidderName.APPNEXUS, + (bidder.openx) : BidderName.OPENX, + (bidder.ix) : BidderName.IX + ].findAll { it.key } + + if (bidderNames.size() != 1) { + throw new IllegalStateException("Invalid number of bidders: ${bidderNames.size()}") + } + + bidderNames.values().first() + } + + @JsonIgnore + List getMediaTypes() { + return [ + (banner ? BANNER : null), + (video ? VIDEO : null), + (nativeObj ? NATIVE : null), + (audio ? AUDIO : null) + ].findAll { it } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy index 774131f0d24..e817a4540a0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy @@ -1,12 +1,14 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.bidder.AppNexus import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.bidder.Rubicon @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class ImpExt { ImpExtPrebid prebid @@ -20,7 +22,12 @@ class ImpExt { ImpExtContextData data String tid String gpid + String sid Integer ae + String all + String skadn + String general + AnyUnsupportedBidder anyUnsupportedBidder static ImpExt getDefaultImpExt() { new ImpExt().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy index bff08aa290b..ffe64b5bcc3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class ImpExtContext { ImpExtContextData data diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy index 34de1ad6575..f171ab15df0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy @@ -2,10 +2,12 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class ImpExtContextData { String language diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy index 21782323974..fc771e59919 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @@ -17,6 +18,7 @@ class ImpExtPrebid { Bidder bidder ImpExtPrebidFloors floors Map passThrough + Map imp static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy new file mode 100644 index 00000000000..a620c7646b1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.EqualsAndHashCode +import org.prebid.server.functional.util.PBSUtils + +@EqualsAndHashCode +class Ix { + + String siteId + List size + String sid + + static Ix getDefault() { + new Ix().tap { + siteId = PBSUtils.randomString + size = [PBSUtils.randomNumber, PBSUtils.randomNumber] + sid = PBSUtils.randomString + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy new file mode 100644 index 00000000000..7bc0adc1054 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class IxDiag { + + String pbsv + String pbjsv + String multipleSiteIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy index ce72554490b..15fb3a6d464 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy @@ -2,12 +2,18 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class Pmp { Integer privateAuction List deals + + static Pmp getDefaultPmp() { + new Pmp(deals: [Deal.defaultDeal]) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index badc1444c18..7240fd91719 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -37,6 +37,8 @@ class Prebid { List adServerTargeting BidderControls bidderControls PrebidModulesConfig modules + PrebidAnalytics analytics + StoredAuctionResponse storedAuctionResponse static class Channel { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy new file mode 100644 index 00000000000..f6ab5331ca7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString +import org.prebid.server.functional.model.config.LogAnalytics + +@ToString(includeNames = true, ignoreNulls = true) +class PrebidAnalytics { + + AnalyticsOptions options + LogAnalytics logAnalytics +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidCacheSettings.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidCacheSettings.groovy index 76f39880e71..fb7ce03436f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidCacheSettings.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidCacheSettings.groovy @@ -1,10 +1,12 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class PrebidCacheSettings { + @JsonProperty("ttlseconds") Integer ttlSeconds Boolean returnCreative } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy index 29f4472cab2..873c686a578 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy @@ -1,10 +1,21 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.config.PriceGranularityType @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class PriceGranularity { Integer precision List ranges + + static PriceGranularity getDefault(PriceGranularityType granularity) { + new PriceGranularity(precision: granularity.precision, ranges: granularity.ranges) + } + + static PriceGranularity getDefault() { + getDefault(PriceGranularityType.MED) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy new file mode 100644 index 00000000000..59fb0b34c25 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.request.auction + +enum PublicCountryIp { + + USA_IP("209.232.44.21", "d646:2414:17b2:f371:9b62:f176:b4c0:51cd"), + UKR_IP("193.238.111.14", "3080:f30f:e4bc:0f56:41be:6aab:9d0a:58e2"), + CAN_IP("70.71.245.39", "f9b2:c742:1922:7d4b:7122:c7fc:8b75:98c8") + + final String v4 + final String v6 + + PublicCountryIp(String v4, String ipV6) { + this.v4 = v4 + this.v6 = ipV6 + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Publisher.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Publisher.groovy index b0778a10cd5..ef0c64fe881 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Publisher.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Publisher.groovy @@ -1,9 +1,11 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Publisher { String id diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Qty.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Qty.groovy index 938b044fd7a..590e232cfb5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Qty.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Qty.groovy @@ -2,10 +2,12 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode class Qty { BigDecimal multiplier diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy index c5fa8cb2220..1b106b67faa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy @@ -1,10 +1,16 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Range { BigDecimal max BigDecimal increment + + static Range getDefault(Integer max, BigDecimal increment) { + new Range(max: max, increment: increment) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RefSettings.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RefSettings.groovy index e00ee5ecdf5..c90adf142ae 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RefSettings.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RefSettings.groovy @@ -2,10 +2,12 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode class RefSettings { RefType refType diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Refresh.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Refresh.groovy index aee58a890d0..397c9053eaa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Refresh.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Refresh.groovy @@ -2,10 +2,12 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode class Refresh { List refSettings diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy index 1d8aa6f077d..6e647c16817 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy @@ -17,7 +17,7 @@ class Regs { static Regs getDefaultRegs() { new Regs().tap { - ext = new RegsExt(gdpr: 0) + gdpr = 0 } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsDsa.groovy deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy index f235dfbd600..9a9c62fde81 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy @@ -8,7 +8,9 @@ import groovy.transform.ToString @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class RegsExt { + @Deprecated(since = "enabling support of ortb 2.6") Integer gdpr + @Deprecated(since = "enabling support of ortb 2.6") String usPrivacy String gpc Dsa dsa diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy new file mode 100644 index 00000000000..8ca88307215 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum SecurityLevel { + + NON_SECURE(0), SECURE(1) + + @JsonValue + private final Integer level + + SecurityLevel(int level) { + this.level = level + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy index 8c21cf3f5a4..cde5f2268de 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy @@ -1,9 +1,15 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString +import org.prebid.server.functional.model.response.auction.SeatBid @ToString(includeNames = true, ignoreNulls = true) class StoredAuctionResponse { String id + @JsonProperty("seatbidarr") + List seatBids + @JsonProperty("seatbidobj") + SeatBid seatBidObject } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Uid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Uid.groovy index e562b9a5423..907c46fce01 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Uid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Uid.groovy @@ -10,6 +10,7 @@ class Uid { String id Integer atype + UidExt ext static Uid getDefaultUid() { new Uid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UidExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UidExt.groovy new file mode 100644 index 00000000000..53ecd62dcbe --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UidExt.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class UidExt { + + String stype +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy index 535e95830d7..0104494910b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy @@ -4,8 +4,6 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils -import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE - @ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode class User { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index 064a5e01b9d..a70ee05eac3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -1,8 +1,11 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Video { List mimes @@ -12,8 +15,10 @@ class Video { Integer maxseq Integer poddur List protocols - Integer w - Integer h + @JsonProperty("w") + Integer weight + @JsonProperty("h") + Integer height Integer podid Integer podseq List rqddurs @@ -38,8 +43,10 @@ class Video { List companionad List api List companiontype + @JsonProperty("poddedupe") + List podDeduplication static Video getDefaultVideo() { - new Video(mimes: ["video/mp4"], w: 300, h: 200) + new Video(mimes: ["video/mp4"], weight: 300, height: 200) } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCachePut.groovy b/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCachePut.groovy new file mode 100644 index 00000000000..45556c3a4d1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCachePut.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.request.cache + +import groovy.transform.ToString +import org.prebid.server.functional.model.mock.services.prebidcache.request.Type +import org.prebid.server.functional.util.ObjectMapperWrapper + +@ToString(includeNames = true, ignoreNulls = true) +class BidCachePut implements ObjectMapperWrapper { + + Type type + CacheBid value + Integer ttlseconds + String bidid + String bidder + Long timestamp + String aid +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCacheRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCacheRequest.groovy new file mode 100644 index 00000000000..ba34fecd79c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCacheRequest.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.cache + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class BidCacheRequest { + + List puts +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/cache/CacheBid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/cache/CacheBid.groovy new file mode 100644 index 00000000000..f614cbf2159 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/cache/CacheBid.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.cache + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.Asset +import org.prebid.server.functional.model.response.auction.Bid + +@ToString(includeNames = true, ignoreNulls = true) +class CacheBid extends Bid { + + List assets + + CacheBid() { + } + + // required for deserialize response in string + CacheBid(String assets) { + this.assets = decode(assets, CacheBid).assets + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/dealsupdate/ForceDealsUpdateRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/dealsupdate/ForceDealsUpdateRequest.groovy deleted file mode 100644 index aaa1c0b2ece..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/request/dealsupdate/ForceDealsUpdateRequest.groovy +++ /dev/null @@ -1,48 +0,0 @@ -package org.prebid.server.functional.model.request.dealsupdate - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString - -import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.CREATE_REPORT -import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.INVALIDATE_LINE_ITEMS -import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.REGISTER_INSTANCE -import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.RESET_ALERT_COUNT -import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.SEND_REPORT -import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.UPDATE_LINE_ITEMS - -@ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) -class ForceDealsUpdateRequest { - - String actionName - - static ForceDealsUpdateRequest getUpdateLineItemsRequest() { - new ForceDealsUpdateRequest(actionName: UPDATE_LINE_ITEMS.name()) - } - - static ForceDealsUpdateRequest getSendReportRequest() { - new ForceDealsUpdateRequest(actionName: SEND_REPORT.name()) - } - - static ForceDealsUpdateRequest getRegisterInstanceRequest() { - new ForceDealsUpdateRequest(actionName: REGISTER_INSTANCE.name()) - } - - static ForceDealsUpdateRequest getResetAlertCountRequest() { - new ForceDealsUpdateRequest(actionName: RESET_ALERT_COUNT.name()) - } - - static ForceDealsUpdateRequest getCreateReportRequest() { - new ForceDealsUpdateRequest(actionName: CREATE_REPORT.name()) - } - - static ForceDealsUpdateRequest getInvalidateLineItemsRequest() { - new ForceDealsUpdateRequest(actionName: INVALIDATE_LINE_ITEMS.name()) - } - - private enum Action { - - UPDATE_LINE_ITEMS, SEND_REPORT, REGISTER_INSTANCE, RESET_ALERT_COUNT, CREATE_REPORT, INVALIDATE_LINE_ITEMS - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy index 9d7000cb7b3..6ed94f08161 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy @@ -24,8 +24,6 @@ class EventRequest { Integer analytics @JsonProperty("ts") Long timestamp - @JsonProperty("l") - String lineItemId static EventRequest getDefaultEventRequest() { def request = new EventRequest() diff --git a/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy b/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy index d18f15880ab..061b5b9d687 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy @@ -5,8 +5,8 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.PgMetrics -import org.prebid.server.functional.model.response.auction.DebugPrivacy import org.prebid.server.functional.model.response.auction.BidderCall +import org.prebid.server.functional.model.response.auction.DebugPrivacy import org.prebid.server.functional.model.response.auction.Trace @ToString(includeNames = true, ignoreNulls = true) diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy index 08c716baa67..1325a65e219 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy @@ -2,8 +2,8 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.ActivityType @ToString(includeNames = true, ignoreNulls = true) diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInvocationPayload.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInvocationPayload.groovy index 960f23af5e9..e44e3c41b40 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInvocationPayload.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInvocationPayload.groovy @@ -2,8 +2,8 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticStags.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticStags.groovy deleted file mode 100644 index 05f9d4ea989..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticStags.groovy +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.functional.model.response.auction - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) -class AnalyticStags { - - List activities -} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsPrebid.groovy new file mode 100644 index 00000000000..dbb360a2f7c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsPrebid.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class AnalyticsPrebid { + + List tags +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsPrebidTag.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsPrebidTag.groovy new file mode 100644 index 00000000000..35d242ded05 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsPrebidTag.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class AnalyticsPrebidTag { + + String stage + String module + List activities + AnalyticsTag analyticsTags +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTag.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTag.groovy new file mode 100644 index 00000000000..d86f4fde95c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTag.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class AnalyticsTag { + + List activities +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivity.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivity.groovy new file mode 100644 index 00000000000..6fd41583617 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivity.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.FetchStatus + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class AnalyticsTagActivity { + + ModuleActivityName name + FetchStatus status + List results +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivityResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivityResult.groovy new file mode 100644 index 00000000000..a439d6411c2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivityResult.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.FetchStatus + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class AnalyticsTagActivityResult { + + FetchStatus status + AnalyticsTagActivityValue values + AppliedTo appliedTo +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivityValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivityValue.groovy new file mode 100644 index 00000000000..7ad629e2b8c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticsTagActivityValue.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AnalyticsTagActivityValue { + + String richmediaFormat +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index ff4a1933d0d..22e29b76908 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -1,5 +1,7 @@ package org.prebid.server.functional.model.response.auction +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.Imp @@ -7,6 +9,7 @@ import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Bid implements ObjectMapperWrapper { String id @@ -29,17 +32,23 @@ class Bid implements ObjectMapperWrapper { List apis Integer api Integer protocol - Integer qagmediarating + @JsonProperty("qagmediarating") + Integer qagMediaRating String language String langb String dealid - Integer w - Integer h - Integer wratio - Integer hratio + @JsonProperty("w") + Integer weight + @JsonProperty("h") + Integer height + @JsonProperty("wratio") + Integer weightRatio + @JsonProperty("hratio") + Integer heightRatio Integer exp Integer dur - Integer mtype + @JsonProperty("mtype") + BidMediaType mediaType Integer slotinpod BidExt ext @@ -53,8 +62,8 @@ class Bid implements ObjectMapperWrapper { impid = imp.id price = PBSUtils.getRandomPrice() crid = 1 - h = imp.banner && imp.banner.format ? imp.banner.format.first().h : null - w = imp.banner && imp.banner.format ? imp.banner.format.first().w : null + height = imp.banner && imp.banner.format ? imp.banner.format.first().height : null + weight = imp.banner && imp.banner.format ? imp.banner.format.first().weight : null if (imp.nativeObj || imp.video) { adm = new Adm(assets: [Asset.defaultAsset]) } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy index 216b78b3609..61cdd7ad5f0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidExt.groovy @@ -9,5 +9,5 @@ class BidExt { Prebid prebid BigDecimal origbidcpm Currency origbidcur - Dsa dsa + DsaResponse dsa } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy new file mode 100644 index 00000000000..76aa2a558f9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum BidMediaType { + + BANNER(1), + VIDEO(2), + AUDIO(3), + NATIVE(4) + + @JsonValue + final Integer value + + BidMediaType(Integer value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index ce517248dca..79cf8ad9317 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy @@ -4,14 +4,25 @@ import com.fasterxml.jackson.annotation.JsonValue enum BidRejectionReason { - NO_BID(0), - TIMED_OUT(101), - REJECTED_BY_HOOK(200), - REJECTED_BY_PRIVACY(202), - REJECTED_BY_MEDIA_TYPE(204), - GENERAL(300), - REJECTED_DUE_TO_PRICE_FLOOR(301), - OTHER_ERROR(100) + ERROR_NO_BID(0), + ERROR_GENERAL(100), + ERROR_TIMED_OUT(101), + ERROR_INVALID_BID_RESPONSE(102), + ERROR_BIDDER_UNREACHABLE(103), + + REQUEST_BLOCKED_GENERAL(200), + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + REQUEST_BLOCKED_PRIVACY(204), + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), + + RESPONSE_REJECTED_GENERAL(300), + RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR(301), + RESPONSE_REJECTED_DUE_TO_DSA(305), + RESPONSE_REJECTED_INVALID_CREATIVE(350), + RESPONSE_REJECTED_INVALID_CREATIVE_SIZE(351), + RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE(352), + RESPONSE_REJECTED_ADVERTISER_BLOCKED(356) @JsonValue final Integer code diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy index caae943a322..fab76cfc309 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy @@ -5,7 +5,6 @@ import groovy.transform.ToString import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.ResponseModel import org.prebid.server.functional.model.bidder.BidderName -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse import org.prebid.server.functional.model.request.auction.BidRequest import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -19,7 +18,7 @@ class BidResponse implements ResponseModel { String bidid Currency cur String customdata - Integer nbr + NoBidResponse nbr BidResponseExt ext static BidResponse getDefaultBidResponse(BidRequest bidRequest, BidderName bidderName = GENERIC) { @@ -29,16 +28,4 @@ class BidResponse implements ResponseModel { bidResponse.seatbid = [seatBid] bidResponse } - - static BidResponse getDefaultPgBidResponse(BidRequest bidRequest, PlansResponse plansResponse) { - def bidResponse = getDefaultBidResponse(bidRequest) - def bid = bidResponse.seatbid[0].bid[0] - def lineItem = plansResponse.lineItems[0] - bid.dealid = lineItem.dealId - if (lineItem.sizes) { - bid.w = lineItem.sizes[0].w - bid.h = lineItem.sizes[0].h - } - bidResponse - } } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponsePrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponsePrebid.groovy index c42b57dc006..aee9daf7783 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponsePrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponsePrebid.groovy @@ -12,4 +12,5 @@ class BidResponsePrebid { Map passThrough ExtBidResponseFledge fledge ExtModule modules + AnalyticsPrebid analytics } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy deleted file mode 100644 index 172955e2e18..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Dsa.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package org.prebid.server.functional.model.response.auction - -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.EqualsAndHashCode -import groovy.transform.ToString -import org.prebid.server.functional.model.request.auction.DsaTransparency -import org.prebid.server.functional.util.PBSUtils - -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) -@EqualsAndHashCode -@ToString(includeNames = true, ignoreNulls = true) -class Dsa { - - String behalf - String paid - List transparency - DsaAdRender adRender - - static Dsa getDefaultDsa() { - new Dsa( - behalf: PBSUtils.randomString, - paid: PBSUtils.randomString, - adRender: PBSUtils.getRandomEnum(DsaAdRender), - transparency: [DsaTransparency.defaultDsaTransparency] - ) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/DsaResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/DsaResponse.groovy new file mode 100644 index 00000000000..30e6fb9accd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/DsaResponse.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.DsaTransparency +import org.prebid.server.functional.util.PBSUtils + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) +class DsaResponse { + + String behalf + String paid + List transparency + DsaAdRender adRender + + static DsaResponse getDefaultDsa() { + new DsaResponse( + behalf: PBSUtils.randomString, + paid: PBSUtils.randomString, + adRender: PBSUtils.getRandomEnum(DsaAdRender), + transparency: [DsaTransparency.defaultDsaTransparency] + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy index 663be4aa6af..267a23cc067 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy @@ -12,7 +12,9 @@ enum ErrorType { PREBID("prebid"), CACHE("cache"), ALIAS("alias"), - TARGETING("targeting") + TARGETING("targeting"), + IX("ix"), + OPENX("openx") @JsonValue final String value diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy index 32ce2fa727d..c0504a64cc4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy @@ -9,4 +9,6 @@ import groovy.transform.ToString class ExtModule { ModuleTrace trace + ModuleError errors + ModuleWarning warnings } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy index 10be533eaf8..3f1594380f4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy @@ -13,5 +13,5 @@ class InvocationResult { InvocationStatus status ResponseAction action HookId hookId - AnalyticStags analyticStags + AnalyticsPrebidTag analyticsTags } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy index f3e376a30dd..5e46ef8425a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy @@ -8,12 +8,15 @@ enum MediaType { VIDEO, AUDIO, NATIVE, + WILDCARD, NULL @JsonValue String getValue() { if (name() == "NULL") { return null + } else if (name() == "WILDCARD") { + return "*" } name().toLowerCase() } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy new file mode 100644 index 00000000000..3942b170875 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum ModuleActivityName { + + ORTB2_BLOCKING('enforce-blocking'), + REJECT_RICHMEDIA('reject-richmedia') + + @JsonValue + final String value + + private ModuleActivityName(String value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy new file mode 100644 index 00000000000..138b5e40507 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class ModuleError { + + Map> ortb2Blocking +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy new file mode 100644 index 00000000000..5c6d4ebed44 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class ModuleWarning { + + Map> ortb2Blocking +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/NoBidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/NoBidResponse.groovy new file mode 100644 index 00000000000..c402d1c2d4a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/NoBidResponse.groovy @@ -0,0 +1,35 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum NoBidResponse { + + UNKNOWN_ERROR(0), + TECHNICAL_ERROR(1), + INVALID_REQUEST(2), + KNOWN_WEB_CRAWLER(3), + SUSPECTED_NON_HUMAN_TRAFFIC(4), + CLOUD_DATA_CENTER_OR_PROXY_IP(5), + UNSUPPORTED_DEVICE(6), + BLOCKED_PUBLISHER_OR_SITE(7), + UNMATCHED_USER(8), + DAILY_USER_CAP_MET(9), + DAILY_DOMAIN_CAP_MET(10), + ADS_TXT_AUTHORIZATION_UNAVAILABLE(11), + ADS_TXT_AUTHORIZATION_VIOLATION(12), + ADS_CART_AUTHORIZATION_UNAVAILABLE(13), + ADS_CART_AUTHORIZATION_VIOLATION(14), + INSUFFICIENT_AUCTION_TIME(15), + INCOMPLETE_SUPPLY_CHAIN(16), + BLOCKED_SUPPLY_CHAIN(17), + EXCHANGE_SPECIFIC_VALUES(500) + + @JsonValue + final Integer nbr + + NoBidResponse(Integer nbr) { + this.nbr = nbr + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleConfiguration.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleConfiguration.groovy index 5cf09375f1e..f15519efb42 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleConfiguration.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleConfiguration.groovy @@ -2,8 +2,8 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming -import groovy.transform.ToString import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString import static org.prebid.server.functional.model.request.auction.Condition.ConditionType diff --git a/src/test/groovy/org/prebid/server/functional/repository/EntityManagerUtil.groovy b/src/test/groovy/org/prebid/server/functional/repository/EntityManagerUtil.groovy index a7712d6430f..cb0a89e3a02 100644 --- a/src/test/groovy/org/prebid/server/functional/repository/EntityManagerUtil.groovy +++ b/src/test/groovy/org/prebid/server/functional/repository/EntityManagerUtil.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.repository -import javax.persistence.EntityManager +import jakarta.persistence.EntityManager import org.hibernate.SessionFactory import java.util.function.Consumer diff --git a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy index 6b39354e62a..d6ee8d65c10 100644 --- a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy +++ b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy @@ -10,22 +10,28 @@ import org.prebid.server.functional.repository.dao.AccountDao import org.prebid.server.functional.repository.dao.StoredImpDao import org.prebid.server.functional.repository.dao.StoredRequestDao import org.prebid.server.functional.repository.dao.StoredResponseDao -import org.testcontainers.containers.MySQLContainer +import org.testcontainers.containers.JdbcDatabaseContainer +import org.testcontainers.containers.PostgreSQLContainer class HibernateRepositoryService { + private static final String MY_SQL_DIALECT = "org.hibernate.dialect.MySQLDialect" + private static final String POSTGRES_SQL_DIALECT = "org.hibernate.dialect.PostgreSQLDialect" + EntityManagerUtil entityManagerUtil AccountDao accountDao StoredImpDao storedImpDao StoredRequestDao storedRequestDao StoredResponseDao storedResponseDao - HibernateRepositoryService(MySQLContainer container) { + HibernateRepositoryService(JdbcDatabaseContainer container) { def jdbcUrl = container.jdbcUrl def user = container.username def pass = container.password def driver = container.driverClassName - SessionFactory sessionFactory = configureHibernate(jdbcUrl, user, pass, driver) + def dialect = container instanceof PostgreSQLContainer ? POSTGRES_SQL_DIALECT : MY_SQL_DIALECT + + SessionFactory sessionFactory = configureHibernate(jdbcUrl, dialect, user, pass, driver) entityManagerUtil = new EntityManagerUtil(sessionFactory) accountDao = new AccountDao(entityManagerUtil) @@ -34,10 +40,14 @@ class HibernateRepositoryService { storedResponseDao = new StoredResponseDao(entityManagerUtil) } - private static SessionFactory configureHibernate(String jdbcUrl, String user, String pass, String driver) { + private static SessionFactory configureHibernate(String jdbcUrl, + String dialect, + String user, + String pass, + String driver) { def properties = new Properties() properties.setProperty("hibernate.connection.url", jdbcUrl) - properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect") + properties.setProperty("hibernate.dialect", dialect) properties.setProperty("hibernate.connection.username", user) properties.setProperty("hibernate.connection.password", pass) properties.setProperty("hibernate.connection.driver_class", driver) diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index 048c81c0c27..bac2badd46a 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -1,7 +1,6 @@ package org.prebid.server.functional.service import com.fasterxml.jackson.core.type.TypeReference -import io.qameta.allure.Step import io.restassured.authentication.AuthenticationScheme import io.restassured.authentication.BasicAuthScheme import io.restassured.builder.RequestSpecBuilder @@ -9,12 +8,10 @@ import io.restassured.response.Response import io.restassured.specification.RequestSpecification import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.bidder.BidderName -import org.prebid.server.functional.model.deals.report.LineItemStatusReport import org.prebid.server.functional.model.mock.services.prebidcache.response.PrebidCacheResponse import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.cookiesync.CookieSyncRequest -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest import org.prebid.server.functional.model.request.event.EventRequest import org.prebid.server.functional.model.request.logging.httpinteraction.HttpInteractionRequest import org.prebid.server.functional.model.request.setuid.SetuidRequest @@ -59,8 +56,6 @@ class PrebidServerService implements ObjectMapperWrapper { static final String CURRENCY_RATES_ENDPOINT = "/currency/rates" static final String HTTP_INTERACTION_ENDPOINT = "/logging/httpinteraction" static final String COLLECTED_METRICS_ENDPOINT = "/collected-metrics" - static final String FORCE_DEALS_UPDATE_ENDPOINT = "/pbs-admin/force-deals-update" - static final String LINE_ITEM_STATUS_ENDPOINT = "/pbs-admin/lineitem-status" static final String PROMETHEUS_METRICS_ENDPOINT = "/metrics" static final String UIDS_COOKIE_NAME = "uids" @@ -82,7 +77,6 @@ class PrebidServerService implements ObjectMapperWrapper { prometheusRequestSpecification = buildAndGetRequestSpecification(pbsContainer.prometheusRootUri, authenticationScheme) } - @Step("[POST] /openrtb2/auction") BidResponse sendAuctionRequest(BidRequest bidRequest, Map headers = [:]) { def response = postAuction(bidRequest, headers) @@ -90,7 +84,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), BidResponse) } - @Step("[POST RAW] /openrtb2/auction") RawAuctionResponse sendAuctionRequestRaw(BidRequest bidRequest, Map headers = [:]) { def response = postAuction(bidRequest, headers) @@ -100,7 +93,13 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[GET] /openrtb2/amp") + AmpResponse sendAmpRequestWithAdditionalQueries(AmpRequest ampRequest, Map queries = [:]) { + def response = getAmp(ampRequest, [:], queries) + + checkResponseStatusCode(response) + decode(response.body.asString(), AmpResponse) + } + AmpResponse sendAmpRequest(AmpRequest ampRequest, Map headers = [:]) { def response = getAmp(ampRequest, headers) @@ -108,7 +107,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), AmpResponse) } - @Step("[GET RAW] /openrtb2/amp") RawAmpResponse sendAmpRequestRaw(AmpRequest ampRequest, Map headers = [:]) { def response = getAmp(ampRequest, headers) @@ -118,7 +116,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[POST] /cookie_sync without cookie") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request) { def response = postCookieSync(request) @@ -126,7 +123,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST] /cookie_sync with headers") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request, Map headers) { def response = postCookieSync(request, null, headers) @@ -134,7 +130,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST] /cookie_sync with uids cookie") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request, UidsCookie uidsCookie) { def response = postCookieSync(request, uidsCookie) @@ -142,7 +137,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST] /cookie_sync with uids and additional cookies") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request, UidsCookie uidsCookie, Map additionalCookies) { @@ -152,7 +146,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST RAW] /cookie_sync with uids cookies") RawCookieSyncResponse sendCookieSyncRequestRaw(CookieSyncRequest request, UidsCookie uidsCookie) { def response = postCookieSync(request, uidsCookie) @@ -162,7 +155,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[POST RAW] /cookie_sync with uids and additional cookies") RawCookieSyncResponse sendCookieSyncRequestRaw(CookieSyncRequest request, UidsCookie uidsCookie, Map additionalCookies) { @@ -174,7 +166,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[GET] /setuid") SetuidResponse sendSetUidRequest(SetuidRequest request, UidsCookie uidsCookie, Map header = [:]) { def uidsCookieAsJson = encode(uidsCookie) def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) @@ -192,7 +183,6 @@ class PrebidServerService implements ObjectMapperWrapper { setuidResponse } - @Step("[GET] /getuids") GetuidResponse sendGetUidRequest(UidsCookie uidsCookie) { def uidsCookieAsJson = encode(uidsCookie) def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) @@ -204,7 +194,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), GetuidResponse) } - @Step("[GET] /event") byte[] sendEventRequest(EventRequest eventRequest, Map headers = [:]) { def response = given(requestSpecification).headers(headers) .queryParams(toMap(eventRequest)) @@ -214,7 +203,6 @@ class PrebidServerService implements ObjectMapperWrapper { response.body.asByteArray() } - @Step("[POST] /vtrack") PrebidCacheResponse sendVtrackRequest(VtrackRequest request, String account) { def response = given(requestSpecification).queryParam("a", account) .body(request) @@ -224,7 +212,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), PrebidCacheResponse) } - @Step("[GET] /status") StatusResponse sendStatusRequest() { def response = given(requestSpecification).get(STATUS_ENDPOINT) @@ -232,7 +219,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), StatusResponse) } - @Step("[GET] /info/bidders") String sendInfoBiddersRequest() { def response = given(requestSpecification).get(INFO_BIDDERS_ENDPOINT) @@ -240,7 +226,6 @@ class PrebidServerService implements ObjectMapperWrapper { response.body().asString() } - @Step("[GET] /info/bidders with params={queryParam}") List sendInfoBiddersRequest(Map queryParam) { def response = given(requestSpecification).queryParams(queryParam).get(INFO_BIDDERS_ENDPOINT) @@ -248,7 +233,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.asString(), new TypeReference>() {}) } - @Step("[GET] /info/bidders/{bidderName}") BidderInfoResponse sendBidderInfoRequest(BidderName bidderName) { def response = given(requestSpecification).get("$INFO_BIDDERS_ENDPOINT/$bidderName.value") @@ -257,7 +241,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), BidderInfoResponse) } - @Step("[GET] /bidders/params") BiddersParamsResponse sendBiddersParamsRequest() { def response = given(requestSpecification).get(BIDDERS_PARAMS_ENDPOINT) @@ -265,7 +248,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), BiddersParamsResponse) } - @Step("[GET] /currency/rates") CurrencyRatesResponse sendCurrencyRatesRequest() { def response = given(adminRequestSpecification).get(CURRENCY_RATES_ENDPOINT) @@ -273,7 +255,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CurrencyRatesResponse) } - @Step("[GET] /logging/httpinteraction") String sendLoggingHttpInteractionRequest(HttpInteractionRequest httpInteractionRequest) { def response = given(adminRequestSpecification).queryParams(toMap(httpInteractionRequest)) .get(HTTP_INTERACTION_ENDPOINT) @@ -282,7 +263,6 @@ class PrebidServerService implements ObjectMapperWrapper { response.body().asString() } - @Step("[GET] /collected-metrics") Map sendCollectedMetricsRequest() { def response = given(adminRequestSpecification).get(COLLECTED_METRICS_ENDPOINT) @@ -290,28 +270,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.asString(), new TypeReference>() {}) } - @Step("[GET] /pbs-admin/force-deals-update") - void sendForceDealsUpdateRequest(ForceDealsUpdateRequest forceDealsUpdateRequest) { - def response = given(adminRequestSpecification).queryParams(toMap(forceDealsUpdateRequest)) - .get(FORCE_DEALS_UPDATE_ENDPOINT) - - checkResponseStatusCode(response, 204) - } - - @Step("[GET] /pbs-admin/lineitem-status") - LineItemStatusReport sendLineItemStatusRequest(String lineItemId) { - def request = given(adminRequestSpecification) - if (lineItemId != null) { - request.queryParam("id", lineItemId) - } - - def response = request.get(LINE_ITEM_STATUS_ENDPOINT) - - checkResponseStatusCode(response) - decode(response.body.asString(), LineItemStatusReport) - } - - @Step("[GET] /metrics") String sendPrometheusMetricsRequest() { def response = given(prometheusRequestSpecification).get(PROMETHEUS_METRICS_ENDPOINT) @@ -363,10 +321,18 @@ class PrebidServerService implements ObjectMapperWrapper { requestSpecification.post(COOKIE_SYNC_ENDPOINT) } - private Response getAmp(AmpRequest ampRequest, Map headers = [:]) { + private Response getAmp(AmpRequest ampRequest, + Map headers = [:], + Map queries = [:]) { + def map = toMap(ampRequest) + + if (!queries.isEmpty()) { + map.putAll(queries) + } + given(requestSpecification).headers(headers) - .queryParams(toMap(ampRequest)) - .get(AMP_ENDPOINT) + .queryParams(map) + .get(AMP_ENDPOINT) } private void checkResponseStatusCode(Response response, int statusCode = 200) { @@ -395,7 +361,7 @@ class PrebidServerService implements ObjectMapperWrapper { if (testEnd.isBefore(testStart)) { throw new IllegalArgumentException("The end time of the test is less than the start time") } - def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") .withZone(ZoneId.from(UTC)) def logs = Arrays.asList(pbsContainer.logs.split("\n")) def filteredLogs = [] @@ -413,6 +379,21 @@ class PrebidServerService implements ObjectMapperWrapper { filteredLogs } + String getLogsByValue(String value) { + if (!value) { + throw new IllegalArgumentException("Value is null or empty") + } + getPbsLogsByValue(value) + } + + Boolean isContainLogsByValue(String value) { + getPbsLogsByValue(value) != null + } + + private String getPbsLogsByValue(String value) { + pbsContainer.logs.split("\n").find { it.contains(value) } + } + T getValueFromContainer(String path, Class clazz) { pbsContainer.copyFileFromContainer(path, { inputStream -> return decode(inputStream, clazz) diff --git a/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy new file mode 100644 index 00000000000..4a25b6d6ca0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy @@ -0,0 +1,103 @@ +package org.prebid.server.functional.service + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.localstack.LocalStackContainer +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.CreateBucketRequest +import software.amazon.awssdk.services.s3.model.DeleteBucketRequest +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import software.amazon.awssdk.services.s3.model.PutObjectResponse + +final class S3Service implements ObjectMapperWrapper { + + private final S3Client s3PbsService + private final LocalStackContainer localStackContainer + + static final def DEFAULT_ACCOUNT_DIR = 'account' + static final def DEFAULT_IMPS_DIR = 'stored-impressions' + static final def DEFAULT_REQUEST_DIR = 'stored-requests' + static final def DEFAULT_RESPONSE_DIR = 'stored-responses' + + S3Service(LocalStackContainer localStackContainer) { + this.localStackContainer = localStackContainer + s3PbsService = S3Client.builder() + .endpointOverride(localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create( + localStackContainer.getAccessKey(), + localStackContainer.getSecretKey()))) + .region(Region.of(localStackContainer.getRegion())) + .build() + } + + String getAccessKeyId() { + localStackContainer.accessKey + } + + String getSecretKeyId() { + localStackContainer.secretKey + } + + String getEndpoint() { + "http://${localStackContainer.getNetworkAliases().get(0)}:${localStackContainer.getExposedPorts().get(0)}" + } + + String getRegion() { + localStackContainer.region + } + + void createBucket(String bucketName) { + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .build() + s3PbsService.createBucket(createBucketRequest) + } + + void deleteBucket(String bucketName) { + DeleteBucketRequest deleteBucketRequest = DeleteBucketRequest.builder() + .bucket(bucketName) + .build() + s3PbsService.deleteBucket(deleteBucketRequest) + } + + void purgeBucketFiles(String bucketName) { + s3PbsService.listObjectsV2(ListObjectsV2Request.builder().bucket(bucketName).build()).contents().each { files -> + s3PbsService.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(files.key()).build()) + } + } + + PutObjectResponse uploadAccount(String bucketName, AccountConfig account, String fileName = account.id) { + uploadFile(bucketName, encode(account), "${DEFAULT_ACCOUNT_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredRequest(String bucketName, StoredRequest storedRequest, String fileName = storedRequest.requestId) { + uploadFile(bucketName, encode(storedRequest.requestData), "${DEFAULT_REQUEST_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredResponse(String bucketName, StoredResponse storedRequest, String fileName = storedRequest.responseId) { + uploadFile(bucketName, encode(storedRequest.storedAuctionResponse), "${DEFAULT_RESPONSE_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredImp(String bucketName, StoredImp storedImp, String fileName = storedImp.impId) { + uploadFile(bucketName, encode(storedImp.impData), "${DEFAULT_IMPS_DIR}/${fileName}.json") + } + + PutObjectResponse uploadFile(String bucketName, String fileBody, String path) { + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(path) + .build() + s3PbsService.putObject(putObjectRequest, RequestBody.fromString(fileBody)) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy index 0798be45cdf..70c99a2a833 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy @@ -4,9 +4,13 @@ import org.prebid.server.functional.testcontainers.container.NetworkServiceConta import org.prebid.server.functional.util.SystemProperties import org.testcontainers.containers.MySQLContainer import org.testcontainers.containers.Network +import org.testcontainers.containers.localstack.LocalStackContainer +import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.lifecycle.Startables +import org.testcontainers.utility.DockerImageName import static org.prebid.server.functional.util.SystemProperties.MOCKSERVER_VERSION +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3 class Dependencies { @@ -20,23 +24,34 @@ class Dependencies { .withDatabaseName("prebid") .withUsername("prebid") .withPassword("prebid") - .withInitScript("org/prebid/server/functional/db_schema.sql") + .withInitScript("org/prebid/server/functional/db_mysql_schema.sql") + .withNetwork(network) + + static final PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer<>("postgres:16.0") + .withDatabaseName("prebid") + .withUsername("prebid") + .withPassword("prebid") + .withInitScript("org/prebid/server/functional/db_psql_schema.sql") .withNetwork(network) static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION) .withNetwork(network) + static LocalStackContainer localStackContainer + static void start() { if (IS_LAUNCH_CONTAINERS) { - Startables.deepStart([networkServiceContainer, mysqlContainer]) - .join() + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) + .withNetwork(network) + .withServices(S3) + Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]).join() } } static void stop() { if (IS_LAUNCH_CONTAINERS) { - [networkServiceContainer, mysqlContainer].parallelStream() - .forEach({ it.stop() }) + [networkServiceContainer, mysqlContainer, localStackContainer].parallelStream() + .forEach({ it.stop() }) } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 66cdb55f34b..aa10ebaf7a4 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.testcontainers import org.testcontainers.containers.MySQLContainer +import org.testcontainers.containers.PostgreSQLContainer import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.testcontainers.container.PrebidServerContainer.ADMIN_ENDPOINT_PASSWORD @@ -95,7 +96,19 @@ LIMIT 1 "settings.database.user" : mysql.username, "settings.database.password" : mysql.password, "settings.database.pool-size" : "2", // setting 2 here to leave some slack for the PBS - "settings.database.provider-class": "hikari" + "settings.database.idle-connection-timeout": "300" + ].asImmutable() + } + + static Map getPostgreSqlConfig(PostgreSQLContainer postgres = Dependencies.postgresqlContainer) { + ["settings.database.type" : "postgres", + "settings.database.host" : postgres.getNetworkAliases().get(0), + "settings.database.port" : postgres.exposedPorts.get(0) as String, + "settings.database.dbname" : postgres.databaseName, + "settings.database.user" : postgres.username, + "settings.database.password" : postgres.password, + "settings.database.pool-size" : "2", // setting 2 here to leave some slack for the PBS + "settings.database.idle-connection-timeout": "300" ].asImmutable() } @@ -105,13 +118,21 @@ LIMIT 1 // due to a config validation we'll need to circumvent all future aliases this way static Map getBidderAliasConfig() { - ["adapters.generic.aliases.cwire.meta-info.site-media-types" : "", - "adapters.generic.aliases.blue.meta-info.app-media-types" : "", - "adapters.generic.aliases.blue.meta-info.site-media-types" : "", - "adapters.generic.aliases.adsinteractive.meta-info.app-media-types" : "", - "adapters.generic.aliases.adsinteractive.meta-info.site-media-types": "", - "adapters.generic.aliases.nativo.meta-info.app-media-types" : "", - "adapters.generic.aliases.nativo.meta-info.site-media-types" : ""] + ["adapters.generic.aliases.cwire.meta-info.site-media-types" : "", + "adapters.generic.aliases.blue.meta-info.app-media-types" : "", + "adapters.generic.aliases.blue.meta-info.site-media-types" : "", + "adapters.generic.aliases.adsinteractive.meta-info.app-media-types" : "", + "adapters.generic.aliases.adsinteractive.meta-info.site-media-types" : "", + "adapters.generic.aliases.nativo.meta-info.app-media-types" : "", + "adapters.generic.aliases.nativo.meta-info.site-media-types" : "", + "adapters.generic.aliases.infytv.meta-info.app-media-types" : "", + "adapters.generic.aliases.infytv.meta-info.site-media-types" : "", + "adapters.generic.aliases.zeta-global-ssp.meta-info.app-media-types" : "", + "adapters.generic.aliases.zeta-global-ssp.meta-info.site-media-types": "", + "adapters.generic.aliases.ccx.meta-info.app-media-types" : "", + "adapters.generic.aliases.ccx.meta-info.site-media-types" : "", + "adapters.generic.aliases.adrino.meta-info.app-media-types" : "", + "adapters.generic.aliases.adrino.meta-info.site-media-types" : ""] } private PbsConfig() {} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsPgConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsPgConfig.groovy deleted file mode 100644 index 47de3679541..00000000000 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsPgConfig.groovy +++ /dev/null @@ -1,135 +0,0 @@ -package org.prebid.server.functional.testcontainers - -import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.testcontainers.container.NetworkServiceContainer -import org.prebid.server.functional.util.PBSUtils - -import java.time.LocalDate - -import static org.prebid.server.functional.testcontainers.scaffolding.pg.Alert.ALERT_ENDPOINT_PATH -import static org.prebid.server.functional.testcontainers.scaffolding.pg.DeliveryStatistics.REPORT_DELIVERY_ENDPOINT_PATH -import static org.prebid.server.functional.testcontainers.scaffolding.pg.GeneralPlanner.PLANS_ENDPOINT_PATH -import static org.prebid.server.functional.testcontainers.scaffolding.pg.GeneralPlanner.REGISTER_ENDPOINT_PATH -import static org.prebid.server.functional.testcontainers.scaffolding.pg.UserData.USER_DETAILS_ENDPOINT_PATH -import static org.prebid.server.functional.testcontainers.scaffolding.pg.UserData.WIN_EVENT_ENDPOINT_PATH - -class PbsPgConfig { - - public static final String PG_ENDPOINT_USERNAME = "pg" - public static final String PG_ENDPOINT_PASSWORD = "pg" - - private static final int NEXT_MONTH = LocalDate.now().plusMonths(1).monthValue - - final Map properties - final String env - final String dataCenter - final String region - final String system - final String subSystem - final String hostId - final String vendor - final Currency currency - final String userIdType - final int maxDealsPerBidder - final int lineItemsPerReport - - PbsPgConfig(NetworkServiceContainer networkServiceContainer) { - properties = getPgConfig(networkServiceContainer.rootUri).asImmutable() - env = properties.get("profile") - dataCenter = properties.get("data-center") - region = properties.get("datacenter-region") - system = properties.get("system") - subSystem = properties.get("sub-system") - hostId = properties.get("host-id") - vendor = properties.get("vendor") - currency = properties.get("auction.ad-server-currency") - userIdType = properties.get("deals.user-data.user-ids[0].type") - maxDealsPerBidder = getIntProperty(properties, "deals.max-deals-per-bidder") - lineItemsPerReport = getIntProperty(properties, "deals.delivery-stats.line-items-per-report") - } - - private static Map getPgConfig(String networkServiceContainerUri) { - pbsGeneralSettings() + adminDealsUpdateEndpoint() + deals() + deliveryProgress() + - planner(networkServiceContainerUri) + deliveryStatistics(networkServiceContainerUri) + - alert(networkServiceContainerUri) + userData(networkServiceContainerUri) + adminLineItemStatusEndpoint() - } - - private static Map pbsGeneralSettings() { - ["host-id" : PBSUtils.randomString, - "datacenter-region" : PBSUtils.randomString, - "vendor" : PBSUtils.randomString, - "profile" : PBSUtils.randomString, - "system" : PBSUtils.randomString, - "sub-system" : PBSUtils.randomString, - "data-center" : PBSUtils.randomString, - "auction.ad-server-currency": "USD", - ] - } - - private static Map adminDealsUpdateEndpoint() { - ["admin-endpoints.force-deals-update.enabled": "true"] - } - - private static Map adminLineItemStatusEndpoint() { - ["admin-endpoints.lineitem-status.enabled": "true"] - } - - private static Map deals() { - ["deals.enabled" : "true", - "deals.simulation.enabled" : "false", - "deals.max-deals-per-bidder": "3" - ] - } - - private static Map planner(String networkServiceContainerUri) { - ["deals.planner.plan-endpoint" : networkServiceContainerUri + PLANS_ENDPOINT_PATH, - "deals.planner.register-endpoint" : networkServiceContainerUri + REGISTER_ENDPOINT_PATH, - "deals.planner.update-period" : "0 15 10 15 $NEXT_MONTH ?" as String, - "deals.planner.plan-advance-period": "0 15 10 15 $NEXT_MONTH ?" as String, - "deals.planner.timeout-ms" : "5000", - "deals.planner.username" : PG_ENDPOINT_USERNAME, - "deals.planner.password" : PG_ENDPOINT_PASSWORD, - "deals.planner.register-period-sec": "3600" - ] - } - - private static Map deliveryStatistics(String networkServiceContainerUri) { - ["deals.delivery-stats.endpoint" : networkServiceContainerUri + - REPORT_DELIVERY_ENDPOINT_PATH, - "deals.delivery-stats.username" : PG_ENDPOINT_USERNAME, - "deals.delivery-stats.password" : PG_ENDPOINT_PASSWORD, - "deals.delivery-stats.delivery-period" : "0 15 10 15 $NEXT_MONTH ?" as String, - "deals.delivery-stats.timeout-ms" : "10000", - "deals.delivery-stats.request-compression-enabled": "false", - "deals.delivery-stats.line-items-per-report" : "5" - ] - } - - private static Map deliveryProgress() { - ["deals.delivery-progress.report-reset-period": "0 15 10 15 $NEXT_MONTH ?" as String] - } - - private static Map alert(String networkServiceContainerUri) { - ["deals.alert-proxy.enabled" : "true", - "deals.alert-proxy.url" : networkServiceContainerUri + ALERT_ENDPOINT_PATH, - "deals.alert-proxy.username" : PG_ENDPOINT_USERNAME, - "deals.alert-proxy.password" : PG_ENDPOINT_PASSWORD, - "deals.alert-proxy.timeout-sec": "10" - ] - } - - private static Map userData(String networkServiceContainerUri) { - ["deals.user-data.user-details-endpoint": networkServiceContainerUri + USER_DETAILS_ENDPOINT_PATH, - "deals.user-data.win-event-endpoint" : networkServiceContainerUri + WIN_EVENT_ENDPOINT_PATH, - "deals.user-data.timeout" : "1000", - "deals.user-data.user-ids[0].type" : "autotest", - "deals.user-data.user-ids[0].source" : "uid", - "deals.user-data.user-ids[0].location" : "generic" - ] - } - - private static getIntProperty(Map properties, String propertyName) { - def property = properties.get(propertyName) - property ? property as int : -1 - } -} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy index d1bbf199ecc..e0911a2b1ca 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy @@ -48,6 +48,12 @@ class PbsServiceFactory { remove(containers) } + static void removeContainer(Map config) { + def container = containers.get(config) + container.stop() + containers.remove(config) + } + private static void remove(Map, PrebidServerContainer> map) { map.each { key, value -> value.stop() @@ -58,6 +64,6 @@ class PbsServiceFactory { private static int getMaxContainerCount() { USE_FIXED_CONTAINER_PORTS ? 1 - : SystemProperties.getPropertyOrDefault("tests.max-container-count", 2) + : SystemProperties.getPropertyOrDefault("tests.max-container-count", 5) } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy index 2d3cbe8abf0..53faa7165fa 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy @@ -8,7 +8,6 @@ class NetworkServiceContainer extends MockServerContainer { NetworkServiceContainer(String version) { super(DockerImageName.parse("mockserver/mockserver:mockserver-$version")) - withCommand("-serverPort $PORT -logLevel WARN") } String getHostAndPort() { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy index 76253716e34..ff0b9f3b4f7 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy @@ -77,7 +77,7 @@ class Bidder extends NetworkScaffolding { def formatNode = it.get("banner") != null ? it.get("banner").get("format") : null new Imp(id: it.get("id").asText(), banner: formatNode != null - ? new Banner(format: [new Format(w: formatNode.first().get("w").asInt(), h: formatNode.first().get("h").asInt())]) + ? new Banner(format: [new Format(weight: formatNode.first().get("w").asInt(), height: formatNode.first().get("h").asInt())]) : null)} def bidRequest = new BidRequest(id: id, imp: imps) def response = BidResponse.getDefaultBidResponse(bidRequest) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy index e50d510d908..224f7c8b228 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy @@ -6,9 +6,9 @@ import org.mockserver.model.HttpRequest import org.mockserver.model.HttpResponse import org.prebid.server.functional.model.mock.services.prebidcache.response.CacheObject import org.prebid.server.functional.model.mock.services.prebidcache.response.PrebidCacheResponse +import org.prebid.server.functional.model.request.cache.BidCacheRequest import org.testcontainers.containers.MockServerContainer -import java.util.stream.Collectors import java.util.stream.Stream import static org.mockserver.model.HttpRequest.request @@ -47,6 +47,15 @@ class PrebidCache extends NetworkScaffolding { .withBody(jsonPath("\$.puts[?(@.value.impid == '$impId')]")) } + List getRecordedRequests(String impId) { + mockServerClient.retrieveRecordedRequests(getRequest(impId)) + .collect { decode(it.body.toString(), BidCacheRequest) } + } + + Map> getRequestHeaders(String impId) { + getLastRecordedRequestHeaders(getRequest(impId)) + } + @Override HttpRequest getRequest() { request().withMethod("POST") diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy index 878944351ff..343a118f53a 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.testcontainers.scaffolding import org.mockserver.matchers.TimeToLive import org.mockserver.matchers.Times +import org.mockserver.model.Delay import org.mockserver.model.HttpRequest import org.mockserver.model.HttpResponse import org.testcontainers.containers.MockServerContainer @@ -9,15 +10,17 @@ import org.testcontainers.containers.MockServerContainer import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V2 +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.Vendor +import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 class VendorList extends NetworkScaffolding { - private static final String VENDOR_LIST_ENDPOINT = "/{TCF_POLICY}/vendor-list.json" + private static final String VENDOR_LIST_ENDPOINT = "/v{TCF_POLICY}/vendor-list.json" VendorList(MockServerContainer mockServerContainer) { super(mockServerContainer, VENDOR_LIST_ENDPOINT) @@ -39,16 +42,20 @@ class VendorList extends NetworkScaffolding { } void setResponse(TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, + Delay delay = null, Map vendors = [(GENERIC_VENDOR_ID): Vendor.getDefaultVendor(GENERIC_VENDOR_ID)]) { - def prepareEndpoint = endpoint.replace("{TCF_POLICY}", "v" + tcfPolicyVersion.vendorListVersion) + def prepareEndpoint = endpoint.replace("{TCF_POLICY}", tcfPolicyVersion.vendorListVersion.toString()) def prepareEncodeResponseBody = encode(defaultVendorListResponse.tap { it.tcfPolicyVersion = tcfPolicyVersion.vendorListVersion it.vendors = vendors + it.gvlSpecificationVersion = tcfPolicyVersion >= TcfPolicyVersion.TCF_POLICY_V4 ? V3 : V2 }) mockServerClient.when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) - .respond { request -> request.withPath(endpoint) - ? response().withStatusCode(OK_200.code()).withBody(prepareEncodeResponseBody) - : HttpResponse.notFoundResponse()} + .respond { request -> + request.withPath(endpoint) + ? response().withStatusCode(OK_200.code()).withDelay(delay).withBody(prepareEncodeResponseBody) + : HttpResponse.notFoundResponse() + } } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/Alert.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/Alert.groovy deleted file mode 100644 index 49efd6b3441..00000000000 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/Alert.groovy +++ /dev/null @@ -1,50 +0,0 @@ -package org.prebid.server.functional.testcontainers.scaffolding.pg - -import com.fasterxml.jackson.core.type.TypeReference -import org.mockserver.model.HttpRequest -import org.prebid.server.functional.model.deals.alert.AlertEvent -import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding -import org.testcontainers.containers.MockServerContainer - -import static org.mockserver.model.HttpRequest.request -import static org.mockserver.model.HttpResponse.response -import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.mockserver.model.JsonPathBody.jsonPath - -class Alert extends NetworkScaffolding { - - static final String ALERT_ENDPOINT_PATH = "/deals/alert" - - Alert(MockServerContainer mockServerContainer) { - super(mockServerContainer, ALERT_ENDPOINT_PATH) - } - - AlertEvent getRecordedAlertRequest() { - def body = getRecordedRequestsBody(request).last() - // 0 index element is returned after deserialization as PBS responses with SingletonList - decode(body, new TypeReference>() {})[0] - } - - Map> getLastRecordedAlertRequestHeaders() { - getLastRecordedRequestHeaders(request) - } - - @Override - void setResponse() { - mockServerClient.when(request().withPath(endpoint)) - .respond(response().withStatusCode(OK_200.code())) - } - - @Override - protected HttpRequest getRequest(String alertId) { - request().withMethod("POST") - .withPath(ALERT_ENDPOINT_PATH) - .withBody(jsonPath("\$[?(@.id == '$alertId')]")) - } - - @Override - protected HttpRequest getRequest() { - request().withMethod("POST") - .withPath(ALERT_ENDPOINT_PATH) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/DeliveryStatistics.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/DeliveryStatistics.groovy deleted file mode 100644 index 32f8f26b4d3..00000000000 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/DeliveryStatistics.groovy +++ /dev/null @@ -1,57 +0,0 @@ -package org.prebid.server.functional.testcontainers.scaffolding.pg - -import org.mockserver.model.HttpRequest -import org.mockserver.model.HttpStatusCode -import org.prebid.server.functional.model.deals.report.DeliveryStatisticsReport -import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding -import org.testcontainers.containers.MockServerContainer - -import static org.mockserver.model.ClearType.ALL -import static org.mockserver.model.HttpRequest.request -import static org.mockserver.model.HttpResponse.response -import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.mockserver.model.JsonPathBody.jsonPath - -class DeliveryStatistics extends NetworkScaffolding { - - static final String REPORT_DELIVERY_ENDPOINT_PATH = "/deals/report/delivery" - - DeliveryStatistics(MockServerContainer mockServerContainer) { - super(mockServerContainer, REPORT_DELIVERY_ENDPOINT_PATH) - } - - Map> getLastRecordedDeliveryRequestHeaders() { - getLastRecordedRequestHeaders(request) - } - - DeliveryStatisticsReport getLastRecordedDeliveryStatisticsReportRequest() { - recordedDeliveryStatisticsReportRequests.last() - } - - void resetRecordedRequests() { - reset(REPORT_DELIVERY_ENDPOINT_PATH, ALL) - } - - void setResponse(HttpStatusCode statusCode = OK_200) { - mockServerClient.when(request().withPath(endpoint)) - .respond(response().withStatusCode(statusCode.code())) - } - - List getRecordedDeliveryStatisticsReportRequests() { - def body = getRecordedRequestsBody(request) - body.collect { decode(it, DeliveryStatisticsReport) } - } - - @Override - protected HttpRequest getRequest(String reportId) { - request().withMethod("POST") - .withPath(REPORT_DELIVERY_ENDPOINT_PATH) - .withBody(jsonPath("\$[?(@.reportId == '$reportId')]")) - } - - @Override - protected HttpRequest getRequest() { - request().withMethod("POST") - .withPath(REPORT_DELIVERY_ENDPOINT_PATH) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/GeneralPlanner.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/GeneralPlanner.groovy deleted file mode 100644 index be27be483f6..00000000000 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/GeneralPlanner.groovy +++ /dev/null @@ -1,96 +0,0 @@ -package org.prebid.server.functional.testcontainers.scaffolding.pg - -import org.mockserver.matchers.Times -import org.mockserver.model.HttpRequest -import org.mockserver.model.HttpStatusCode -import org.prebid.server.functional.model.deals.register.RegisterRequest -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding -import org.testcontainers.containers.MockServerContainer - -import static org.mockserver.model.HttpRequest.request -import static org.mockserver.model.HttpResponse.response -import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.mockserver.model.JsonPathBody.jsonPath - -class GeneralPlanner extends NetworkScaffolding { - - static final String PLANS_ENDPOINT_PATH = "/deals/plans" - static final String REGISTER_ENDPOINT_PATH = "/deals/register" - - GeneralPlanner(MockServerContainer mockServerContainer) { - super(mockServerContainer, REGISTER_ENDPOINT_PATH) - } - - void initRegisterResponse(HttpStatusCode statusCode = OK_200) { - reset() - setResponse(statusCode) - } - - void initPlansResponse(PlansResponse plansResponse, - HttpStatusCode statusCode = OK_200, - Times times = Times.exactly(1)) { - resetPlansEndpoint() - setPlansResponse(plansResponse, statusCode, times) - } - - void resetPlansEndpoint() { - reset(PLANS_ENDPOINT_PATH) - } - - int getRecordedPlansRequestCount() { - getRequestCount(plansRequest) - } - - RegisterRequest getLastRecordedRegisterRequest() { - recordedRegisterRequests.last() - } - - List getRecordedRegisterRequests() { - def body = getRecordedRequestsBody(request) - body.collect { decode(it, RegisterRequest) } - } - - void setResponse(HttpStatusCode statusCode = OK_200) { - mockServerClient.when(request().withPath(endpoint)) - .respond(response().withStatusCode(statusCode.code())) - } - - Map> getLastRecordedRegisterRequestHeaders() { - getLastRecordedRequestHeaders(request) - } - - Map> getLastRecordedPlansRequestHeaders() { - getLastRecordedRequestHeaders(plansRequest) - } - - @Override - void reset() { - super.reset(PLANS_ENDPOINT_PATH) - super.reset(REGISTER_ENDPOINT_PATH) - } - - private void setPlansResponse(PlansResponse plansResponse, - HttpStatusCode statusCode, - Times times = Times.exactly(1)) { - setResponse(plansRequest, plansResponse, statusCode, times) - } - - @Override - protected HttpRequest getRequest(String hostInstanceId) { - request().withMethod("POST") - .withPath(REGISTER_ENDPOINT_PATH) - .withBody(jsonPath("\$[?(@.hostInstanceId == '$hostInstanceId')]")) - } - - @Override - protected HttpRequest getRequest() { - request().withMethod("POST") - .withPath(REGISTER_ENDPOINT_PATH) - } - - private static HttpRequest getPlansRequest() { - request().withMethod("GET") - .withPath(PLANS_ENDPOINT_PATH) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/UserData.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/UserData.groovy deleted file mode 100644 index 9a0caafa140..00000000000 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/UserData.groovy +++ /dev/null @@ -1,77 +0,0 @@ -package org.prebid.server.functional.testcontainers.scaffolding.pg - -import org.mockserver.model.HttpRequest -import org.mockserver.model.HttpStatusCode -import org.prebid.server.functional.model.deals.userdata.UserDetailsRequest -import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse -import org.prebid.server.functional.model.deals.userdata.WinEventNotification -import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding -import org.testcontainers.containers.MockServerContainer - -import static org.mockserver.model.HttpRequest.request -import static org.mockserver.model.HttpResponse.response -import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.mockserver.model.JsonPathBody.jsonPath - -class UserData extends NetworkScaffolding { - - static final String USER_DETAILS_ENDPOINT_PATH = "/deals/user-details" - static final String WIN_EVENT_ENDPOINT_PATH = "/deals/win-event" - - UserData(MockServerContainer mockServerContainer) { - super(mockServerContainer, WIN_EVENT_ENDPOINT_PATH) - } - - UserDetailsRequest getRecordedUserDetailsRequest() { - def body = getRecordedRequestsBody(userDetailsRequest).last() - decode(body, UserDetailsRequest) - } - - WinEventNotification getRecordedWinEventRequest() { - def body = getRecordedRequestsBody(request).last() - decode(body, WinEventNotification) - } - - void setUserDataResponse(UserDetailsResponse userDataResponse, HttpStatusCode httpStatusCode = OK_200) { - resetUserDetailsEndpoint() - setResponse(userDetailsRequest, userDataResponse, httpStatusCode) - } - - int getRecordedUserDetailsRequestCount() { - getRequestCount(userDetailsRequest) - } - - void resetUserDetailsEndpoint() { - reset(USER_DETAILS_ENDPOINT_PATH) - } - - @Override - void reset() { - super.reset(USER_DETAILS_ENDPOINT_PATH) - super.reset(WIN_EVENT_ENDPOINT_PATH) - } - - @Override - void setResponse() { - mockServerClient.when(request().withPath(endpoint)) - .respond(response().withStatusCode(OK_200.code())) - } - - @Override - protected HttpRequest getRequest(String bidId) { - request().withMethod("POST") - .withPath(WIN_EVENT_ENDPOINT_PATH) - .withBody(jsonPath("\$[?(@.bidId == '$bidId')]")) - } - - @Override - protected HttpRequest getRequest() { - request().withMethod("POST") - .withPath(WIN_EVENT_ENDPOINT_PATH) - } - - private static HttpRequest getUserDetailsRequest() { - request().withMethod("POST") - .withPath(USER_DETAILS_ENDPOINT_PATH) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy index 61c510518f5..15cd0668f6c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy @@ -130,9 +130,7 @@ class AliasSpec extends BaseSpec { def "PBS aliased bidder config should be independently from parent"() { given: "Pbs config" - def prebidServerService = pbsServiceFactory.getService( - ["adapters.generic.aliases.alias.enabled" : "true", - "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + def prebidServerService = pbsServiceFactory.getService(GENERIC_ALIAS_CONFIG) and: "Default bid request with alias" def bidRequest = BidRequest.defaultBidRequest.tap { diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy index 555eec86e4e..ac3ba146010 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -84,7 +84,7 @@ class AmpSpec extends BaseSpec { then: "Response should contain information from stored response" def price = storedAuctionResponse.bid[0].price assert response.targeting["hb_pb"] == getRoundedTargetingValueWithDefaultPrecision(price) - assert response.targeting["hb_size"] == "${storedAuctionResponse.bid[0].w}x${storedAuctionResponse.bid[0].h}" + assert response.targeting["hb_size"] == "${storedAuctionResponse.bid[0].weight}x${storedAuctionResponse.bid[0].height}" and: "PBS not send request to bidder" assert bidder.getRequestCount(ampStoredRequest.id) == 0 @@ -107,7 +107,7 @@ class AmpSpec extends BaseSpec { and: "Default stored request with specified: gdpr, debug" def ampStoredRequest = BidRequest.defaultStoredRequest - ampStoredRequest.regs.ext.gdpr = 1 + ampStoredRequest.regs.gdpr = 1 and: "Stored request in DB" def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) @@ -122,8 +122,8 @@ class AmpSpec extends BaseSpec { assert bidderRequest.site?.page == ampRequest.curl assert bidderRequest.site?.publisher?.id == ampRequest.account.toString() assert bidderRequest.imp[0]?.tagId == ampRequest.slot - assert bidderRequest.imp[0]?.banner?.format*.h == [ampRequest.h, msH] - assert bidderRequest.imp[0]?.banner?.format*.w == [ampRequest.w, msW] + assert bidderRequest.imp[0]?.banner?.format*.height == [ampRequest.h, msH] + assert bidderRequest.imp[0]?.banner?.format*.weight == [ampRequest.w, msW] assert bidderRequest.regs?.gdpr == (ampRequest.gdprApplies ? 1 : 0) } @@ -150,8 +150,8 @@ class AmpSpec extends BaseSpec { then: "Bidder request should contain parameters from request" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - assert bidderRequest.imp[0]?.banner?.format*.h == [ampRequest.oh] - assert bidderRequest.imp[0]?.banner?.format*.w == [ampRequest.ow] + assert bidderRequest.imp[0]?.banner?.format*.height == [ampRequest.oh] + assert bidderRequest.imp[0]?.banner?.format*.weight == [ampRequest.ow] } def "PBS should take parameters from the stored request when it's not specified in the request"() { @@ -176,8 +176,8 @@ class AmpSpec extends BaseSpec { assert bidderRequest.site?.page == ampStoredRequest.site.page assert bidderRequest.site?.publisher?.id == ampStoredRequest.site.publisher.id assert !bidderRequest.imp[0]?.tagId - assert bidderRequest.imp[0]?.banner?.format[0]?.h == ampStoredRequest.imp[0].banner.format[0].h - assert bidderRequest.imp[0]?.banner?.format[0]?.w == ampStoredRequest.imp[0].banner.format[0].w - assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.ext.gdpr + assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height + assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy index c3b6f9e76b0..b113f649834 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy @@ -1,7 +1,13 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.config.AccountAnalyticsConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AnalyticsModule +import org.prebid.server.functional.model.config.LogAnalytics +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.mock.services.pubstack.PubStackResponse import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.PrebidAnalytics import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.Dependencies import org.prebid.server.functional.testcontainers.PbsConfig @@ -13,7 +19,15 @@ import spock.lang.Shared class AnalyticsSpec extends BaseSpec { private static final String SCOPE_ID = UUID.randomUUID() + private static final Map ENABLED_DEBUG_LOG_MODE = ["logging.level.root": "debug"] private static final PrebidServerService pbsService = pbsServiceFactory.getService(PbsConfig.getPubstackAnalyticsConfig(SCOPE_ID)) + private static final PrebidServerService pbsServiceWithLogAnalytics = pbsServiceFactory.getService( + ENABLED_DEBUG_LOG_MODE + ['analytics.log.enabled' : 'true', + 'analytics.global.adapters': 'logAnalytics']) + private static final PrebidServerService pbsServiceWithoutLogAnalytics = pbsServiceFactory.getService( + ENABLED_DEBUG_LOG_MODE + ['analytics.log.enabled' : 'true', + 'analytics.global.adapters': '']) + @Shared PubStackAnalytics analytics = new PubStackAnalytics(Dependencies.networkServiceContainer).tap { @@ -34,4 +48,216 @@ class AnalyticsSpec extends BaseSpec { then: "PBS should call pubstack analytics" PBSUtils.waitUntil { analytics.requestCount == analyticsRequestCount + 1 } } + + def "PBS should populate log analytics when logging enabled in global config but not in account config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: null)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "Analytics bid request shouldn't be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + } + + def "PBS shouldn't populate log analytics when log analytics is directly non-restricted for account and disabled in global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + then: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS should populate log analytics when log analytics is directly non-restricted for account and enabled global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Analytics bid request shouldn't be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS shouldn't populate log analytics when log analytics is directly restricted for account and enabled in global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: false) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS shouldn't populate log analytics when log disabled in global config and not set for account"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: null)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS should populate log analytics with additional data when log is directly non-restricted for account and data specified"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics() + } + + and: "Account in the DB" + def additionalData = PBSUtils.randomString + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: additionalData) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics.logAnalytics + + then: "Analytics bid request should be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS should populate log analytics with additional data from request when data specified in request only"() { + given: "Basic bid request" + def additionalData = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics(logAnalytics: new LogAnalytics(additionalData: additionalData)) + } + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: null) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Analytics bid request should be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + + where: + logAnalyticsEnable << [null, true] + } + + def "PBS should prioritize logAnalytics from request when data specified in account and request"() { + given: "Basic bid request" + def bidRequestAdditionalData = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics(logAnalytics: new LogAnalytics(additionalData: bidRequestAdditionalData)) + } + + and: "Account in the DB" + def accountAdditionalData = PBSUtils.randomString + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: accountAdditionalData) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "Analytics bid request should be emitted in logs" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == bidRequestAdditionalData + + where: + logAnalyticsEnable << [null, true] + } + + private static BidRequest extractResolvedRequestFromLog(String logsByText) { + decode(logsByText.split("resolvedrequest")[1] + .replace(";", "") + .replaceFirst(":", "") + .replaceFirst("\"", ""), BidRequest.class) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy index cfab2bafde1..83a726d2035 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -45,7 +45,7 @@ class AuctionSpec extends BaseSpec { private static final boolean CORS_SUPPORT = false private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT private static final int DEFAULT_TIMEOUT = getRandomTimeout() - private static final Map PBS_CONFIG = ["auction.max-timeout-ms" : MAX_TIMEOUT as String, + private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] private static final Map GENERIC_CONFIG = [ "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, @@ -410,12 +410,10 @@ class AuctionSpec extends BaseSpec { def "PBS call to alias should populate bidder request buyeruid from family user.buyeruids when it's contained in base bidder"() { given: "Pbs config with alias" def cookieName = PBSUtils.randomString - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_CONFIG + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_CONFIG + GENERIC_ALIAS_CONFIG + ["host-cookie.family" : GENERIC.value, "host-cookie.cookie-name" : cookieName, - "adapters.generic.usersync.cookie-family-name": GENERIC.value, - "adapters.generic.aliases.alias.enabled" : "true", - "adapters.generic.aliases.alias.endpoint" : "$networkServiceContainer.rootUri/auction".toString()]) + "adapters.generic.usersync.cookie-family-name": GENERIC.value]) and: "Alias bid request" def buyeruid = PBSUtils.randomString diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index f681998f033..fcb735d7186 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -1,5 +1,9 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.bidderspecific.BidderRequest +import org.prebid.server.functional.model.response.amp.AmpResponse +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.BidderCall import org.prebid.server.functional.repository.HibernateRepositoryService import org.prebid.server.functional.repository.dao.AccountDao import org.prebid.server.functional.repository.dao.StoredImpDao @@ -36,6 +40,8 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { private static final int MIN_TIMEOUT = DEFAULT_TIMEOUT private static final int DEFAULT_TARGETING_PRECISION = 1 private static final String DEFAULT_CACHE_DIRECTORY = "/app/prebid-server/data" + protected static final Map GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] protected final PrebidServerService defaultPbsService = pbsServiceFactory.getService([:]) @@ -77,4 +83,21 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { protected static String getRoundedTargetingValueWithDefaultPrecision(BigDecimal value) { "${value.setScale(DEFAULT_TARGETING_PRECISION, DOWN)}0" } + + protected static Map> getRequests(BidResponse bidResponse) { + bidResponse.ext.debug.bidders.collectEntries { bidderName, bidderCalls -> + collectRequestByBidderName(bidderName, bidderCalls) + } + } + + protected static Map> getRequests(AmpResponse ampResponse) { + ampResponse.ext.debug.bidders.collectEntries { bidderName, bidderCalls -> + collectRequestByBidderName(bidderName, bidderCalls) + } + } + + private static LinkedHashMap> collectRequestByBidderName(String bidderName, + List bidderCalls) { + [(bidderName): bidderCalls.collect { bidderCall -> decode(bidderCall.requestBody as String, BidderRequest) }] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy new file mode 100644 index 00000000000..cc877f847a0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy @@ -0,0 +1,364 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.PrebidCache +import org.prebid.server.functional.model.request.auction.PrebidCacheSettings +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +class BidExpResponseSpec extends BaseSpec { + + private static def hostBannerTtl = PBSUtils.randomNumber + private static def hostVideoTtl = PBSUtils.randomNumber + private static def cacheTtlService = pbsServiceFactory.getService(['cache.banner-ttl-seconds': hostBannerTtl as String, + 'cache.video-ttl-seconds' : hostVideoTtl as String]) + + def "PBS auction should resolve bid.exp from response that is set by the bidder’s adapter"() { + given: "Default basicResponse with exp" + def bidResponseExp = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = bidResponseExp + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidResponseExp] + + and: "PBS should not call PBC" + assert !prebidCache.getRequestCount(bidRequest.imp.first.id) + + where: + bidRequest << [BidRequest.defaultBidRequest, BidRequest.defaultVideoRequest] + } + + def "PBS auction should resolve bid.exp from response and send it to cache when it set by the bidder’s adapter and cache enabled for request"() { + given: "BidRequest with enabled cache" + bidRequest.enableCache() + + and: "Default basic bid with exp" + def bidResponseExp = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = bidResponseExp + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidResponseExp] + + and: "PBS should call PBC" + def cacheRequests = prebidCache.getRecordedRequests(bidRequest.imp.first.id) + assert cacheRequests.puts.first.first.ttlseconds == bidResponseExp + + where: + bidRequest << [BidRequest.defaultBidRequest, BidRequest.defaultVideoRequest] + } + + def "PBS auction should resolve exp from request.imp[].exp when it have value"() { + given: "Default basic bidRequest with exp" + def bidRequestExp = PBSUtils.randomNumber + bidRequest.tap { + imp.first.exp = bidRequestExp + } + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidRequestExp] + + where: + bidRequest << [BidRequest.defaultBidRequest, BidRequest.defaultVideoRequest] + } + + def "PBS auction should resolve exp from request.ext.prebid.cache.bids for banner request when it have value"() { + given: "Default basic bid with ext.prebid.cache.bids" + def bidRequestExp = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + enableCache() + ext.prebid.cache.bids = new PrebidCacheSettings(ttlSeconds: bidRequestExp) + } + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidRequestExp] + } + + def "PBS auction should resolve exp from request.ext.prebid.cache.vastxml for video request when it have value"() { + given: "Default basic bid with ext.prebid.cache.vastXml" + def bidRequestExp = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultVideoRequest.tap { + enableCache() + ext.prebid.cache.vastXml = new PrebidCacheSettings(ttlSeconds: bidRequestExp) + } + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidRequestExp] + } + + def "PBS auction shouldn't resolve exp from request.ext.prebid.cache for request when it have invalid type"() { + given: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + bidRequest | cache + BidRequest.defaultBidRequest | new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + BidRequest.defaultVideoRequest | new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + def "PBS auction should resolve exp from account config for banner request when it have value"() { + given: "default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountCacheTtl = PBSUtils.randomNumber + def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [accountCacheTtl] + } + + def "PBS auction shouldn't resolve exp from account videoCacheTtl config when bidRequest type doesn't matching"() { + given: "default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + } + + def "PBS auction should resolve exp from account videoCacheTtl config for video request when it have value"() { + given: "default bidRequest" + def bidRequest = BidRequest.defaultVideoRequest + + and: "Account in the DB" + def accountCacheTtl = PBSUtils.randomNumber + def auctionConfig = new AccountAuctionConfig(videoCacheTtl: accountCacheTtl) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [accountCacheTtl] + } + + def "PBS auction should resolve exp from account bannerCacheTtl config for video request when it have value"() { + given: "default bidRequest" + def bidRequest = BidRequest.defaultVideoRequest + + and: "Account in the DB" + def accountCacheTtl = PBSUtils.randomNumber + def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [accountCacheTtl] + } + + def "PBS auction should resolve exp from global banner config for banner request"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + when: "PBS processes auction request" + def response = cacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [hostBannerTtl] + } + + def "PBS auction should resolve exp from global config for video request based on highest value"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultVideoRequest + + and: "Set bidder response without exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = cacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [Math.max(hostVideoTtl, hostBannerTtl)] + } + + def "PBS auction should prioritize value from bid.exp rather than request.imp[].exp"() { + given: "Default basic bidRequest with exp" + bidRequest.tap { + imp.first.exp = PBSUtils.randomNumber + } + + and: "Set bidder response with exp" + def bidResponseExp = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = bidResponseExp + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidResponseExp] + + where: + bidRequest << [BidRequest.defaultBidRequest, BidRequest.defaultVideoRequest] + } + + def "PBS auction should prioritize value from request.imp[].exp rather than request.ext.prebid.cache"() { + given: "Default basic bidRequest with exp" + def bidRequestExp = PBSUtils.randomNumber + def bidRequestBidsCacheExp = PBSUtils.randomNumber + bidRequest.tap { + enableCache() + imp.first.exp = bidRequestExp + ext.prebid.cache.bids = new PrebidCacheSettings(ttlSeconds: bidRequestBidsCacheExp) + } + + and: "Set bidder response with exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidRequestExp] + + where: + bidRequest << [BidRequest.defaultBidRequest, BidRequest.defaultVideoRequest] + } + + def "PBS auction should prioritize value from request.ext.prebid.cache rather than account config"() { + given: "Default basic bidRequest with exp" + def bidRequestBidsCacheExp = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + enableCache() + ext.prebid.cache.bids = new PrebidCacheSettings(ttlSeconds: bidRequestBidsCacheExp) + } + + and: "Account in the DB" + def accountCacheTtl = PBSUtils.randomNumber + def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + and: "Set bidder response with exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [bidRequestBidsCacheExp] + } + + def "PBS auction should prioritize value from account config rather than host config"() { + given: "Default basic bidRequest with exp" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountCacheTtl = PBSUtils.randomNumber + def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + and: "Set bidder response with exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = cacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [accountCacheTtl] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy index fb3e2a3b0aa..b2cda35dde1 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy @@ -269,12 +269,15 @@ class BidValidationSpec extends BaseSpec { } bidder.setResponse(bidRequest.id, bidResponse) + and: "Flush metric" + flushMetrics(defaultPbsService) + when: "Sending auction request to PBS" defaultPbsService.sendAuctionRequest(bidRequest) then: "Bid validation metric value is incremented" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics["adapter.generic.requests.bid_validation"] == initialMetricValue + 1 + assert metrics["adapter.generic.requests.bid_validation"] == 1 } def "PBS shouldn't throw error when two separate eids with same eids.source"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy new file mode 100644 index 00000000000..bd7d6abfafc --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy @@ -0,0 +1,871 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountBidValidationConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.BidValidationEnforcement +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.Asset +import org.prebid.server.functional.model.request.auction.Audio +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Format +import org.prebid.server.functional.model.request.auction.Native +import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.request.auction.Video +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature +import spock.lang.Shared + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE +import static org.prebid.server.functional.model.config.BidValidationEnforcement.SKIP +import static org.prebid.server.functional.model.config.BidValidationEnforcement.WARN +import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC + +class BidderFormatSpec extends BaseSpec { + + @Shared + private static final RANDOM_NUMBER = PBSUtils.randomNumber + + def "PBS should successfully pass when banner.format weight and height is valid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [new Format(weight: bannerFormatWeight, height: bannerFormatHeight)] + } + + when: "Requesting PBS auction" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidResponse should contain the same banner format as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.imp[0]?.banner?.format[0].weight == bannerFormatWeight + assert bidderRequest?.imp[0]?.banner?.format[0].height == bannerFormatHeight + + where: + bannerFormatWeight | bannerFormatHeight + 1 | 1 + PBSUtils.randomNumber | PBSUtils.randomNumber + } + + def "PBS should unsuccessfully pass and throw error due to validation banner.format{w.h} when banner.format weight or height is invalid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [new Format(weight: bannerFormatWeight, height: bannerFormatHeight)] + } + + when: "Requesting PBS auction" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to banner.format{w.h} validation" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == "Invalid request format: " + + "request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties" + + where: + bannerFormatWeight | bannerFormatHeight + 0 | PBSUtils.randomNumber + PBSUtils.randomNumber | 0 + null | PBSUtils.randomNumber + PBSUtils.randomNumber | null + PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + PBSUtils.randomNumber | PBSUtils.randomNegativeNumber + } + + def "PBS should unsuccessfully pass and throw error due to validation banner.format{w.h} when banner.format weight and height is invalid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [new Format(weight: bannerFormatWeight, height: bannerFormatHeight)] + } + + when: "Requesting PBS auction" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to banner.format{w.h} validation" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == "Invalid request format: request.imp[0].banner.format[0] " + + "should define *either* {w, h} (for static size requirements) " + + "*or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive" + + where: + bannerFormatWeight | bannerFormatHeight + 0 | 0 + 0 | null + 0 | PBSUtils.randomNegativeNumber + null | null + null | PBSUtils.randomNegativeNumber + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber + } + + def "PBS should successfully pass when banner weight and height is valid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner = new Banner(weight: bannerFormatWeight, height: bannerFormatHeight) + } + + when: "Requesting PBS auction" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidResponse should contain the same banner{w.h} as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.imp[0]?.banner?.weight == bannerFormatWeight + assert bidderRequest?.imp[0]?.banner?.height == bannerFormatHeight + + where: + bannerFormatWeight | bannerFormatHeight + 1 | 1 + PBSUtils.randomNumber | PBSUtils.randomNumber + } + + def "PBS should unsuccessfully pass and throw error due to validation banner{w.h} when banner{w.h} is invalid"() { + given: "Default bid request with banner{w.h}" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner = new Banner(weight: bannerFormatWeight, height: bannerFormatHeight) + } + + when: "Requesting PBS auction" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to banner{w.h} validation" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == "Invalid request format: " + + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements" + + where: + bannerFormatWeight | bannerFormatHeight + 0 | 0 + 0 | PBSUtils.randomNumber + PBSUtils.randomNumber | 0 + null | null + null | PBSUtils.randomNumber + PBSUtils.randomNumber | null + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber + PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + PBSUtils.randomNumber | PBSUtils.randomNegativeNumber + } + + def "PBS should emit error and metrics when banner-creative-max-size: warn and bid response W or H is larger that request W or H"() { + given: "PBS with banner creative max size" + def pbsService = pbsServiceFactory.getService(["auction.validations.banner-creative-max-size": configCreativeMaxSize]) + + and: "Default bid request with banner format" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = new Banner(format: [new Format(weight: RANDOM_NUMBER, height: RANDOM_NUMBER)]) + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response with biggest W and H than in bidRequest in DB" + def storedBidId = UUID.randomUUID() + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + it.id = storedBidId + it.weight = responseWeight + it.height = responseHeight + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with specified banner max size enforcement" + def account = getAccountWithSpecifiedBannerMax(bidRequest.accountId, accountCretiveMaxSize) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsService) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics["account.${bidRequest.accountId}.response.validation.size.warn"] == 1 + assert metrics["adapter.generic.response.validation.size.warn"] == 1 + + and: "Response should contain error" + assert bidResponse.ext?.errors[GENERIC]*.code == [5] + assert bidResponse.ext?.errors[GENERIC]*.message[0] + == "BidId `${storedBidId}` validation messages: " + + "Warning: BidResponse validation `warn`: bidder `${GENERIC}` response triggers creative size " + + "validation for bid ${storedBidId}, account=${bidRequest.accountId}, " + + "referrer=${bidRequest.site.page}, max imp size='${RANDOM_NUMBER}x${RANDOM_NUMBER}', " + + "bid response size='${responseWeight}x${responseHeight}'" + + and: "Bid response should contain weight and height from stored response" + def bid = bidResponse.seatbid[0].bid[0] + assert bid.weight == responseWeight + assert bid.height == responseHeight + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + accountCretiveMaxSize | configCreativeMaxSize | responseWeight | responseHeight + null | WARN.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | WARN.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | WARN.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + WARN | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + WARN | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + WARN | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + } + + def "PBS shouldn't emit error and metrics when banner-creative-max-size: skip and bid response W or H is larger that request W or H"() { + given: "PBS with banner creative max size" + def pbsService = pbsServiceFactory.getService(["auction.validations.banner-creative-max-size": configCreativeMaxSize]) + + and: "Default bid request with banner format" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = new Banner(format: [new Format(weight: RANDOM_NUMBER, height: RANDOM_NUMBER)]) + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response with biggest W and H than in bidRequest in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + it.weight = responseWeight + it.height = responseHeight + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with specified banner max size enforcement" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(bidValidationsSnakeCase: + new AccountBidValidationConfig(bannerMaxSizeEnforcementSnakeCase: accountCretiveMaxSizeSnakeCase, bannerMaxSizeEnforcement: accountCretiveMaxSize), debugAllow: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric shouldn't increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.size.warn"] + assert !metrics["account.${bidRequest.accountId}.response.validation.size.err"] + + and: "Response should contain error" + assert !bidResponse.ext?.errors + + and: "Bid response should contain weight and height from stored response" + def bid = bidResponse.seatbid[0].bid[0] + assert bid.weight == responseWeight + assert bid.height == responseHeight + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + accountCretiveMaxSizeSnakeCase | accountCretiveMaxSize | configCreativeMaxSize | responseWeight | responseHeight + null | null | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | null | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | null | SKIP.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + null | SKIP | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | SKIP | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | SKIP | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + null | null | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | null | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | null | SKIP.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + SKIP | null | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + SKIP | null | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + SKIP | null | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + } + + def "PBS should emit error and metrics and remove bid response from consideration when banner-creative-max-size: enforce and bid response W or H is larger that request W or H"() { + given: "PBS with banner creative max size" + def pbsService = pbsServiceFactory.getService(["auction.validations.banner-creative-max-size": configCreativeMaxSize]) + + and: "Default bid request with banner format" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = new Banner(format: [new Format(weight: RANDOM_NUMBER, height: RANDOM_NUMBER)]) + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response with biggest W and H than in bidRequest in DB" + def storedBidId = UUID.randomUUID() + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + it.id = storedBidId + it.weight = responseWeight + it.height = responseHeight + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with specified banner max size enforcement" + def account = getAccountWithSpecifiedBannerMax(bidRequest.accountId, accountCretiveMaxSize) + accountDao.save(account) + + and: + flushMetrics(pbsService) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics["account.${bidRequest.accountId}.response.validation.size.err"] == 1 + assert metrics["adapter.generic.response.validation.size.err"] == 1 + + and: "Response should contain error" + assert bidResponse.ext?.errors[GENERIC]*.code == [5] + assert bidResponse.ext?.errors[GENERIC]*.message[0] + == "BidId `${storedBidId}` validation messages: " + + "Error: BidResponse validation `enforce`: bidder `${GENERIC.value}` response triggers creative size " + + "validation for bid ${storedBidId}, account=${bidRequest.accountId}, " + + "referrer=${bidRequest.site.page}, max imp size='${RANDOM_NUMBER}x${RANDOM_NUMBER}', " + + "bid response size='${responseWeight}x${responseHeight}'" + + and: "Pbs should discard seatBid due to validation" + assert !bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + accountCretiveMaxSize | configCreativeMaxSize | responseWeight | responseHeight + null | ENFORCE.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | ENFORCE.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | ENFORCE.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + ENFORCE | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + ENFORCE | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + ENFORCE | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + } + + def "PBS shouldn't emit error and metrics when banner-creative-max-size #configCreativeMaxSize and bid response W or H is same that request W or H"() { + given: "PBS with banner creative max size" + def pbsService = pbsServiceFactory.getService(["auction.validations.banner-creative-max-size": configCreativeMaxSize]) + + and: "Default bid request with banner format" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = new Banner(format: [new Format(weight: RANDOM_NUMBER, height: RANDOM_NUMBER)]) + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response with biggest W and H than in bidRequest in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + weight = RANDOM_NUMBER + height = RANDOM_NUMBER + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with specified banner max size enforcement" + def account = getAccountWithSpecifiedBannerMax(bidRequest.accountId, accountCretiveMaxSize) + accountDao.save(account) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric shouldn't increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.size.warn"] + assert !metrics["account.${bidRequest.accountId}.response.validation.size.err"] + + and: "Response should contain error" + assert !bidResponse.ext?.errors + + and: "Bid response should contain weight and height from stored response" + def bid = bidResponse.seatbid[0].bid[0] + assert bid.weight == RANDOM_NUMBER + assert bid.height == RANDOM_NUMBER + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + accountCretiveMaxSize | configCreativeMaxSize + null | SKIP.value + SKIP | null + ENFORCE | null + null | ENFORCE.value + WARN | null + null | WARN.value + } + + def "PBS shouldn't emit error and metrics when media type isn't banner and banner-creative-max-size #configCreativeMaxSize and bid response W or H is larger that request W or H"() { + given: "PBS with banner creative max size" + def pbsService = pbsServiceFactory.getService(["auction.validations.banner-creative-max-size": configCreativeMaxSize]) + + and: "Default bid request with video W and H" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + imp[0].tap { + video = new Video(weight: RANDOM_NUMBER, height: RANDOM_NUMBER, mimes: [PBSUtils.randomString]) + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response with biggest W and H than in bidRequest in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + weight = responseWeight + height = responseHeight + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with specified banner max size enforcement" + def account = getAccountWithSpecifiedBannerMax(bidRequest.accountId, accountCretiveMaxSize) + accountDao.save(account) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.size.err"] + assert !metrics["adapter.generic.response.validation.size.err"] + + and: "Response shouldn't contain error" + assert !bidResponse.ext?.errors + + and: "Pbs should contain seatBid.bid" + assert bidResponse.seatbid.bid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + accountCretiveMaxSize | configCreativeMaxSize | responseWeight | responseHeight + null | ENFORCE.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | ENFORCE.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | ENFORCE.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + ENFORCE | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + ENFORCE | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + ENFORCE | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + null | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | SKIP.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + SKIP | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + SKIP | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + SKIP | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + null | WARN.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + null | WARN.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + null | WARN.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + WARN | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + WARN | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + WARN | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + } + + def "PBS should emit error and metrics and remove bid response from consideration and account value should take precedence over host when banner-creative-max-size enforce and bid response W or H is larger that request W or H"() { + given: "PBS with banner creative max size" + def pbsService = pbsServiceFactory.getService(["auction.validations.banner-creative-max-size": configCreativeMaxSize]) + + and: "Default bid request with banner format" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = new Banner(format: [new Format(weight: RANDOM_NUMBER, height: RANDOM_NUMBER)]) + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response with biggest W and H than in bidRequest in DB" + def storedBidId = UUID.randomUUID() + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + it.id = storedBidId + it.weight = responseWeight + it.height = responseHeight + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with specified banner max size enforcement" + def account = getAccountWithSpecifiedBannerMax(bidRequest.accountId, accountCretiveMaxSize) + accountDao.save(account) + + and: + flushMetrics(pbsService) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics["account.${bidRequest.accountId}.response.validation.size.err"] == 1 + assert metrics["adapter.generic.response.validation.size.err"] == 1 + + and: "Bid response should contain error" + assert bidResponse.ext?.errors[GENERIC]*.code == [5] + assert bidResponse.ext?.errors[GENERIC]*.message[0] + == "BidId `${storedBidId}` validation messages: " + + "Error: BidResponse validation `enforce`: bidder `generic` response triggers creative size " + + "validation for bid ${storedBidId}, account=${bidRequest.accountId}, " + + "referrer=${bidRequest.site.page}, max imp size='${RANDOM_NUMBER}x${RANDOM_NUMBER}', " + + "bid response size='${responseWeight}x${responseHeight}'" + + and: "Pbs should discard seatBid due to validation" + assert !bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + accountCretiveMaxSize | configCreativeMaxSize | responseWeight | responseHeight + ENFORCE | WARN.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + ENFORCE | WARN.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + ENFORCE | WARN.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + ENFORCE | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + ENFORCE | null | RANDOM_NUMBER + 1 | RANDOM_NUMBER + ENFORCE | null | RANDOM_NUMBER | RANDOM_NUMBER + 1 + ENFORCE | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + 1 + ENFORCE | SKIP.value | RANDOM_NUMBER + 1 | RANDOM_NUMBER + ENFORCE | SKIP.value | RANDOM_NUMBER | RANDOM_NUMBER + 1 + } + + @PendingFeature(reason = "Waiting for confirmation") + def "PBS shouldn't make a validation for audio media type when secure is #secure and secure markUp is #secureMarkup"() { + given: "PBS with secure-markUp: #secureMarkup" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": secureMarkup]) + + and: "Audio bid request" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].secure = secure + imp[0].banner = null + imp[0].video = null + imp[0].audio = Audio.defaultAudio + imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = new Adm(assets: [Asset.getImgAsset("http://secure-assets.${PBSUtils.randomString}.com")]) + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric shouldn't be increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.warn"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.warn"] + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.err"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.err"] + + and: "Bid response should contain error" + assert !bidResponse.ext?.errors + + and: "Pbs should contain seatBid" + assert bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + secure | secureMarkup + SECURE | SKIP.value + SECURE | ENFORCE.value + SECURE | WARN.value + NON_SECURE | SKIP.value + NON_SECURE | ENFORCE.value + NON_SECURE | WARN.value + } + + def "PBS should emit metrics and error when imp[0].secure = 1 and config WARN and bid response adm contain #url"() { + given: "PBS with secure-markUp: warn" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": WARN.value]) + + and: "Default bid request with secure and banner or video or nativeObj" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].secure = SECURE + imp[0].banner = banner + imp[0].video = video + imp[0].nativeObj = nativeObj + imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + + and: "Stored bid response in DB" + def storedBidId = UUID.randomUUID() + def adm = new Adm(assets: [Asset.getImgAsset("${url}://secure-assets.${PBSUtils.randomString}.com")]) + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + it.id = storedBidId + it.adm = adm + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics["account.${bidRequest.accountId}.response.validation.secure.warn"] == 1 + assert metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.warn"] == 1 + + and: "Bid response should contain error" + assert bidResponse.ext?.errors[GENERIC]*.code == [5] + assert bidResponse.ext?.errors[GENERIC]*.message[0] + == "BidId `${storedBidId}` validation messages: " + + "Warning: BidResponse validation `warn`: bidder `${BidderName.GENERIC.value}` response triggers secure creative " + + "validation for bid ${storedBidId}, account=${bidRequest.accountId}, referrer=${bidRequest.site.page}," + + " adm=${encode(adm)}" + + and: "Pbs should contain seatBid" + assert bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + url | banner | video | nativeObj + "http%3A" | Banner.defaultBanner | null | null + "http" | Banner.defaultBanner | null | null + "http" | null | Video.defaultVideo | null + "http%3A" | null | Video.defaultVideo | null + "http" | null | null | Native.defaultNative + "http%3A" | null | null | Native.defaultNative + } + + def "PBS should emit metrics and error when imp[0].secure = 1, banner and config SKIP and bid response adm contain #url"() { + given: "PBS with secure-markUp: skip" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": SKIP.value]) + + and: "Default bid request with secure and banner or video or nativeObj" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].secure = SECURE + imp[0].banner = banner + imp[0].video = video + imp[0].nativeObj = nativeObj + imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + + and: "Stored bid response in DB with adm" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = new Adm(assets: [Asset.getImgAsset("${url}://secure-assets.${PBSUtils.randomString}.com")]) + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.warn"] + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.err"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.warn"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.err"] + + and: "Bid response shouldn't contain error" + assert !bidResponse.ext?.errors + + and: "Pbs should contain seatBid" + assert bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + url | banner | video | nativeObj + "http%3A" | Banner.defaultBanner | null | null + "http" | Banner.defaultBanner | null | null + "http" | null | Video.defaultVideo | null + "http%3A" | null | Video.defaultVideo | null + "http" | null | null | Native.defaultNative + "http%3A" | null | null | Native.defaultNative + } + + def "PBS should emit metrics and error and remove bid response when imp[0].secure = 1, banner and config ENFORCE and bid response adm contain #url"() { + given: "PBS with secure-markUp: enforce" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": ENFORCE.value]) + + and: "Default bid request with secure and banner or video or nativeObj" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].secure = SECURE + imp[0].banner = banner + imp[0].video = video + imp[0].nativeObj = nativeObj + imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + + and: "Stored bid response in DB" + def storedBidId = UUID.randomUUID() + def adm = new Adm(assets: [Asset.getImgAsset("${url}://secure-assets.${PBSUtils.randomString}.com")]) + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].tap { + it.id = storedBidId + it.adm = adm + } + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric should increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics["account.${bidRequest.accountId}.response.validation.secure.err"] == 1 + assert metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.err"] == 1 + + and: "Bid response should contain error" + assert bidResponse.ext?.errors[GENERIC]*.code == [5] + assert bidResponse.ext?.errors[GENERIC]*.message[0] + == "BidId `${storedBidId}` validation messages: " + + "Error: BidResponse validation `enforce`: bidder `${BidderName.GENERIC.value}` response triggers secure creative " + + "validation for bid ${storedBidId}, account=${bidRequest.accountId}, referrer=${bidRequest.site.page}," + + " adm=${encode(adm)}" + + and: "Pbs shouldn't contain seatBid" + assert !bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + url | banner | video | nativeObj + "http%3A" | Banner.defaultBanner | null | null + "http" | Banner.defaultBanner | null | null + "http" | null | Video.defaultVideo | null + "http%3A" | null | Video.defaultVideo | null + "http" | null | null | Native.defaultNative + "http%3A" | null | null | Native.defaultNative + } + + def "PBS shouldn't emit errors and metrics when imp[0].secure = #secure and bid response adm contain #url"() { + given: "PBS with secure-markUp" + def pbsService = pbsServiceFactory + .getService(["auction.validations.secure-markup": secureMarkup]) + + and: "Default bid request with secure" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.secure = secure + it.ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response in DB with adm" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = new Adm(assets: [Asset.getImgAsset("${url}://secure-assets.${PBSUtils.randomString}.com")]) + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric shouldn't increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.warn"] + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.err"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.warn"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.err"] + + and: "Bid response shouldn't contain error" + assert !bidResponse.ext?.errors + + and: "Pbs should contain seatBid" + assert bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + url | secure | secureMarkup + "http%3A" | NON_SECURE | SKIP.value + "http" | NON_SECURE | SKIP.value + "https" | SECURE | SKIP.value + "http%3A" | NON_SECURE | WARN.value + "http" | NON_SECURE | WARN.value + "https" | SECURE | WARN.value + "http%3A" | NON_SECURE | ENFORCE.value + "http" | NON_SECURE | ENFORCE.value + "https" | SECURE | ENFORCE.value + } + + def "PBS should ignore specified secureMarkup #secureMarkup validation when secure is 0"() { + given: "PBS with secure-markUp" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": secureMarkup]) + + and: "Default bid request with stored bid response and secure" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + secure = NON_SECURE + ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] + } + } + + and: "Stored bid response in DB with adm" + def adm = new Adm(assets: [Asset.getImgAsset("${url}://secure-assets.${PBSUtils.randomString}.com")]) + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = adm + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "Requesting PBS auction" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Corresponding metric shouldn't increments" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics["account.${bidRequest.accountId}.response.validation.secure.warn"] + assert !metrics["adapter.${BidderName.GENERIC.value}.response.validation.secure.warn"] + + and: "Bid response shouldn't contain error" + assert !bidResponse.ext?.errors + + and: "Pbs should contain seatBid" + assert bidResponse.seatbid + + and: "PBs shouldn't perform a bidder request due to stored bid response" + assert !bidder.getBidderRequests(bidRequest.id) + + where: + secureMarkup | url + WARN.value | "http" + WARN.value | "http%3A" + WARN.value | "https" + ENFORCE.value | "http" + ENFORCE.value | "http%3A" + ENFORCE.value | "https" + SKIP.value | "https" + SKIP.value | "http%3A" + SKIP.value | "https" + } + + private static Account getAccountWithSpecifiedBannerMax(String accountId, BidValidationEnforcement bannerMaxSizeEnforcement) { + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig( + bidValidations: new AccountBidValidationConfig(bannerMaxSizeEnforcement: bannerMaxSizeEnforcement), + debugAllow: true)) + new Account(status: ACTIVE, uuid: accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index 14467df1967..cab50bd816b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1,6 +1,6 @@ package org.prebid.server.functional.tests -import io.qameta.allure.Issue +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredImp @@ -9,34 +9,45 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Banner import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.AnyUnsupportedBidder import org.prebid.server.functional.model.request.auction.Geo import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpExt +import org.prebid.server.functional.model.request.auction.ImpExtContext +import org.prebid.server.functional.model.request.auction.ImpExtContextData import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidStoredRequest -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent +import static org.prebid.server.functional.model.Currency.CHF +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.CompressionType.GZIP import static org.prebid.server.functional.model.bidder.CompressionType.NONE import static org.prebid.server.functional.model.request.auction.Asset.titleAsset import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY +import static org.prebid.server.functional.model.response.auction.ErrorType.ALIAS +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.HttpUtil.CONTENT_ENCODING_HEADER import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED @@ -53,7 +64,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain httpcalls" - assert response.ext?.debug?.httpcalls[GENERIC.value] + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] and: "Response should not contain error" assert !response.ext?.errors @@ -79,7 +90,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [2] + assert response.ext?.errors[GENERIC]*.code == [2] where: adapterDefault | generic | adapterConfig @@ -154,7 +165,7 @@ class BidderParamsSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - bidRequest.regs.ext = new RegsExt(usPrivacy: validCcpa) + bidRequest.regs.usPrivacy = validCcpa def lat = PBSUtils.getRandomDecimal(0, 90) def lon = PBSUtils.getRandomDecimal(0, 90) bidRequest.device = new Device(geo: new Geo(lat: lat, lon: lon)) @@ -181,7 +192,7 @@ class BidderParamsSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - bidRequest.regs.ext = new RegsExt(usPrivacy: validCcpa) + bidRequest.regs.usPrivacy = validCcpa def lat = PBSUtils.getRandomDecimal(0, 90) as float def lon = PBSUtils.getRandomDecimal(0, 90) as float bidRequest.device = new Device(geo: new Geo(lat: lat, lon: lon)) @@ -207,7 +218,7 @@ class BidderParamsSpec extends BaseSpec { bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(firstParam: firstParam) and: "Set bidderParam to bidRequest" - bidRequest.ext.prebid.bidderParams = [(GENERIC): [firstParam: PBSUtils.randomNumber]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [firstParam: PBSUtils.randomNumber]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -242,7 +253,7 @@ class BidderParamsSpec extends BaseSpec { and: "Set bidderParam to bidRequest" def secondParam = PBSUtils.randomNumber - bidRequest.ext.prebid.bidderParams = [(GENERIC): [secondParam: secondParam]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [secondParam: secondParam]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -271,7 +282,6 @@ class BidderParamsSpec extends BaseSpec { } // TODO: create same test for enabled circuit breaker - @Issue("https://github.com/prebid/prebid-server-java/issues/1478") def "PBS should emit warning when bidder endpoint is invalid"() { given: "Pbs config" def pbsService = pbsServiceFactory.getService(["adapters.generic.enabled" : "true", @@ -285,8 +295,8 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [999] - assert response.ext?.errors[ErrorType.GENERIC]*.message == ["no empty host accepted"] + assert response.ext?.errors[GENERIC]*.code == [999] + assert response.ext?.errors[GENERIC]*.message == ["host name must not be empty"] } def "PBS should reject bidder when bidder params from request doesn't satisfy json-schema for auction request"() { @@ -391,8 +401,8 @@ class BidderParamsSpec extends BaseSpec { assert response.seatbid.isEmpty() and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == ["Bidder does not support any media types."] + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Bidder does not support any media types."] where: configMediaType | bidRequest @@ -508,8 +518,8 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequest.imp[0].nativeObj and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from the " + "request for this bidder." as String] @@ -527,7 +537,7 @@ class BidderParamsSpec extends BaseSpec { def bidResponse = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert bidResponse.ext?.warnings[ErrorType.GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") + assert bidResponse.ext?.warnings[GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") and: "Bid response shouldn't contain any seatbid" assert !bidResponse.seatbid @@ -561,8 +571,8 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert response.ext?.warnings[ErrorType.GENERIC]?.message == - ["Imp ${bidRequest.imp[1].id} does not have a supported media type and has been removed from the request for this bidder." ] + assert response.ext?.warnings[GENERIC]?.message == + ["Imp ${bidRequest.imp[1].id} does not have a supported media type and has been removed from the request for this bidder."] and: "Bid response should contain seatbid" assert response.seatbid @@ -596,8 +606,8 @@ class BidderParamsSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 0 and: "Response should contain errors" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2, 2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2, 2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from " + "the request for this bidder.", "Bid request contains 0 impressions after filtering."] @@ -642,7 +652,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain header Content-Encoding = gzip" - assert response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER)?.first() == compressionType } @@ -658,7 +668,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should not contain header Content-Encoding" - assert !response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert !response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER) } @@ -699,9 +709,9 @@ class BidderParamsSpec extends BaseSpec { where: secureStoredRequest | secureBidderRequest - null | 1 - 1 | 1 - 0 | 0 + null | SECURE + SECURE | SECURE + NON_SECURE | NON_SECURE } def "PBS auction should populate imp[0].secure depend which value in imp request"() { @@ -719,8 +729,315 @@ class BidderParamsSpec extends BaseSpec { where: secureRequest | secureBidderRequest - null | 1 - 1 | 1 - 0 | 0 + null | SECURE + SECURE | SECURE + NON_SECURE | NON_SECURE + } + + def "PBS shouldn't emit warning and proceed auction when imp.ext.anyUnsupportedBidder and imp.ext.prebid.bidder.generic in the request"() { + given: "Default bid request" + def unsupportedBidder = new AnyUnsupportedBidder(anyUnsupportedField: PBSUtils.randomString) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.anyUnsupportedBidder = unsupportedBidder + imp[0].ext.prebid.bidder.generic = new Generic() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain imp.ext.anyUnsupportedBidder" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ext.anyUnsupportedBidder == unsupportedBidder + + and: "Response shouldn't contain warning" + assert !response?.ext?.warnings + } + + def "PBS should emit warning and proceed auction when imp.ext.anyUnsupportedBidder and imp.ext.generic in the request"() { + given: "Default bid request" + def unsupportedBidder = new AnyUnsupportedBidder(anyUnsupportedField: PBSUtils.randomString) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.generic = new Generic() + imp[0].ext.anyUnsupportedBidder = unsupportedBidder + imp[0].ext.prebid.bidder = null + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain imp.ext.anyUnsupportedBidder" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ext.anyUnsupportedBidder == unsupportedBidder + + and: "PBS should emit an warning" + assert response?.ext?.warnings[PREBID]*.code == [999] + assert response?.ext?.warnings[PREBID]*.message == + ["WARNING: request.imp[0].ext.prebid.bidder.anyUnsupportedBidder was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder contains unknown bidder: anyUnsupportedBidder"] + } + + def "PBS shouldn't emit warning and proceed auction when all imp.ext fields known for PBS"() { + given: "Default bid request with populated imp.ext" + def impExt = ImpExt.getDefaultImpExt().tap { + prebid.bidder.generic = null + generic = new Generic() + ae = PBSUtils.randomNumber + all = PBSUtils.randomNumber + context = new ImpExtContext(data: new ImpExtContextData()) + data = new ImpExtContextData(pbAdSlot: PBSUtils.randomString) + general = PBSUtils.randomString + gpid = PBSUtils.randomString + skadn = PBSUtils.randomString + tid = PBSUtils.randomString + } + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext = impExt + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain same field as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.imp[0].ext) { + bidder == impExt.generic + ae == impExt.ae + all == impExt.all + context == impExt.context + data == impExt.data + general == impExt.general + gpid == impExt.gpid + skadn == impExt.skadn + tid == impExt.tid + } + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "") + + and: "Default bid request with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "") + + and: "Default bid request with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + and: "PBS should emit an warnings" + assert response.ext?.warnings[GENERIC]*.code == [999] + assert response.ext?.warnings[GENERIC]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.aliases.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "PBS should emit an warnings" + assert response.ext?.warnings[ALIAS]*.code == [999] + assert response.ext?.warnings[ALIAS]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.ALIAS.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index 3f9ccc7ab43..4f8dcf7675e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -1,5 +1,9 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountEventsConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp @@ -15,6 +19,8 @@ import static org.prebid.server.functional.model.response.auction.MediaType.VIDE class CacheSpec extends BaseSpec { + private final static String PBS_API_HEADER = 'x-pbc-api-key' + def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { given: "Current value of metric prebid_cache.requests.ok" def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok") @@ -83,6 +89,51 @@ class CacheSpec extends BaseSpec { then: "PBS should call PBC" assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call shouldn't include api-key" + assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + } + + def "PBS should cache bids without api-key header when targeting is specified and api-key-secured disabled"() { + given: "Pbs config with disabled api-key-secured and pbc.api.key" + def apiKey = PBSUtils.randomString + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'false']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.enableCache() + bidRequest.ext.prebid.targeting = new Targeting() + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + prebidCache.getRequest() + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call shouldn't include api-key" + assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + } + + def "PBS should cache bids with api-key header when targeting is specified and api-key-secured enabled"() { + given: "Pbs config with api-key-secured and pbc.api.key" + def apiKey = PBSUtils.randomString + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'true']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.enableCache() + bidRequest.ext.prebid.targeting = new Targeting() + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + prebidCache.getRequest() + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call should include api-key" + assert prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] == [apiKey] } def "PBS should not cache bids when targeting isn't specified"() { @@ -192,4 +243,50 @@ class CacheSpec extends BaseSpec { false | BANNER true | VIDEO } + + def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok") + + and: "Create and save enabled events config in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + uuid = accountId + config = new AccountConfig().tap { + auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true)) + } + } + accountDao.save(account) + + and: "Vtrack request with custom tags" + def payload = PBSUtils.randomString + def creative = "<${wrapper}>prebid.org wrapper" + + "<![CDATA[//${payload}]]>" + + "<${impression}> <![CDATA[ ]]> " + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, accountId) + + then: "Vast xml is modified" + def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) + assert prebidCacheRequest.size() == 1 + assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + + and: "prebid_cache.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics["prebid_cache.requests.ok"] == initialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics["account.${accountId}.prebid_cache.requests.ok" as String] == 1 + + where: + wrapper | impression + " wrapper " | " impression " + PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ") + " wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}" + " inLine " | " ImpreSSion $PBSUtils.randomNumber" + PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " + " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy index 5bbed9602d6..5beaa13bac4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CookieSyncSpec.groovy @@ -1,7 +1,6 @@ //file:noinspection GroovyGStringKey package org.prebid.server.functional.tests -import org.prebid.server.functional.model.AccountStatus import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountAuctionConfig @@ -27,12 +26,13 @@ import org.prebid.server.functional.util.privacy.TcfConsent import java.time.Instant import java.util.concurrent.TimeUnit +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.bidder.BidderName.AAX import static org.prebid.server.functional.model.bidder.BidderName.ACEEX import static org.prebid.server.functional.model.bidder.BidderName.ACUITYADS import static org.prebid.server.functional.model.bidder.BidderName.ADKERNEL import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.AAX import static org.prebid.server.functional.model.bidder.BidderName.BOGUS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.OPENX @@ -395,8 +395,6 @@ class CookieSyncSpec extends BaseSpec { } and: "Save account with cookie config" - def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: 1) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -414,6 +412,10 @@ class CookieSyncSpec extends BaseSpec { assert bogusBidderStatus?.error == "Unsupported bidder" assert bogusBidderStatus?.noCookie == null assert bogusBidderStatus?.userSync == null + + where: + accountConfig << [new AccountConfig(status: ACTIVE, cookieSync: new AccountCookieSyncConfig(defaultLimit: 1)), + new AccountConfig(status: ACTIVE, cookieSyncSnakeCase: new AccountCookieSyncConfig(defaultLimit: 1))] } def "PBS cookie sync request should reflect error even when response is full by PBS config limit"() { @@ -845,7 +847,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie sync config" def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: 2) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -872,7 +874,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def accountDefaultLimit = 1 def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: accountDefaultLimit) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -959,9 +961,8 @@ class CookieSyncSpec extends BaseSpec { } and: "Save account with cookie sync config" - def maxLimit = 2 - def cookieSyncConfig = new AccountCookieSyncConfig(maxLimit: maxLimit) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def cookieSyncConfig = new AccountCookieSyncConfig(maxLimit: accountMaxLimit, maxLimitSnakeCase: accountMaxLimitSnakeCase) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -969,7 +970,12 @@ class CookieSyncSpec extends BaseSpec { def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) then: "Response should contain only two synced bidder" - assert response.bidderStatus.size() == maxLimit + assert response.bidderStatus.size() == 2 + + where: + accountMaxLimit | accountMaxLimitSnakeCase + 2 | null + null | 2 } def "PBS cookie sync with cookie-sync.pri and enabled coop-sync in config should sync bidder which present in cookie-sync.pri config"() { @@ -1031,7 +1037,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: false)) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -1145,7 +1151,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: false)) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -1204,7 +1210,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(pri: [bidderName.value], coopSync: new AccountCoopSyncConfig(enabled: true)) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -1234,7 +1240,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(pri: [bidderName.value]) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -1264,7 +1270,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: true)) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -1998,7 +2004,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: 0) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2021,7 +2027,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def maxLimit = 1 def cookieSyncConfig = new AccountCookieSyncConfig(maxLimit: maxLimit, defaultLimit: 2) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2036,16 +2042,13 @@ class CookieSyncSpec extends BaseSpec { given: "Default cookie sync request" def accountId = PBSUtils.randomNumber def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - bidders = [GENERIC, BOGUS] + bidders = [GENERIC, APPNEXUS, ADKERNEL] account = accountId - limit = 2 + limit = null debug = false } and: "Save account with cookie config" - def maxLimit = 1 - def cookieSyncConfig = new AccountCookieSyncConfig(maxLimit: maxLimit) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2053,7 +2056,11 @@ class CookieSyncSpec extends BaseSpec { def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) then: "Response should contain corresponding bidders size due to config" - assert response.bidderStatus.size() == maxLimit + assert response.bidderStatus.size() == 2 + + where: + accountConfig << [new AccountConfig(status: ACTIVE, cookieSyncSnakeCase: new AccountCookieSyncConfig(maxLimit: 2)), + new AccountConfig(status: ACTIVE, cookieSync: new AccountCookieSyncConfig(maxLimit: 2))] } def "PBS cookie sync request should capped to max limit"() { @@ -2069,7 +2076,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def maxLimit = 1 def cookieSyncConfig = new AccountCookieSyncConfig(maxLimit: maxLimit) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2094,9 +2101,8 @@ class CookieSyncSpec extends BaseSpec { } and: "Save account with cookie config" - def defaultLimit = 1 - def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: defaultLimit) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: accountDefaultLimit, defaultLimitSnakeCase: accountDefaultLimitSnakeCase) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2104,7 +2110,12 @@ class CookieSyncSpec extends BaseSpec { def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) then: "Response should contain corresponding bidders size due to config" - assert response.bidderStatus.size() == defaultLimit + assert response.bidderStatus.size() == 1 + + where: + accountDefaultLimit | accountDefaultLimitSnakeCase + 1 | null + null | 1 } def "PBS cookie sync request should take precedence request limit over account and global config"() { @@ -2123,7 +2134,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie config" def cookieSyncConfig = new AccountCookieSyncConfig(defaultLimit: 2) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2144,8 +2155,7 @@ class CookieSyncSpec extends BaseSpec { } and: "Save account with cookie config" - def cookieSyncConfig = new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: accountCoopSyncConfig)) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, cookieSync: cookieSyncConfig) + def accountConfig = new AccountConfig(status: ACTIVE, cookieSync: cookieSyncConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2156,7 +2166,12 @@ class CookieSyncSpec extends BaseSpec { assert response.bidderStatus.size() == 9 where: - accountCoopSyncConfig << [false, true, null] + cookieSyncConfig << [new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: true)), + new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: false)), + new AccountCookieSyncConfig(coopSync: new AccountCoopSyncConfig(enabled: null)), + new AccountCookieSyncConfig(coopSyncSnakeCase: new AccountCoopSyncConfig(enabled: true)), + new AccountCookieSyncConfig(coopSyncSnakeCase: new AccountCoopSyncConfig(enabled: false)), + new AccountCookieSyncConfig(coopSyncSnakeCase: new AccountCoopSyncConfig(enabled: null))] } def "PBS cookie sync request should respond with an error when gdpr param is 1 and consent isn't specified"() { @@ -2186,7 +2201,7 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie and privacySandbox configs" def accountAuctionConfig = new AccountAuctionConfig(privacySandbox: privacySandbox) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, auction: accountAuctionConfig) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) @@ -2215,15 +2230,15 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie and privacySandbox configs" def privacySandbox = PrivacySandbox.defaultPrivacySandbox def accountAuctionConfig = new AccountAuctionConfig(privacySandbox: privacySandbox) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, auction: accountAuctionConfig) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) when: "PBS processes cookie sync request" - def setCookieDefaultHeader = ['receive-cookie-deprecation': '1'] + def setCookieDefaultHeader = ['receive-cookie-deprecation': '1'] def response = prebidServerService.sendCookieSyncRequestRaw(cookieSyncRequest, uidsCookie, setCookieDefaultHeader) - then: "Response shouldn't contain cookie header" + then: "Response shouldn't contain cookie header" assert !response.headers[SET_COOKIE_HEADER] } @@ -2239,16 +2254,16 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie and privacySandbox configs" def accountAuctionConfig = new AccountAuctionConfig(privacySandbox: privacySandbox) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, auction: accountAuctionConfig) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) when: "PBS processes cookie sync request" def response = prebidServerService.sendCookieSyncRequestRaw(cookieSyncRequest, uidsCookie) - then: "Response should contain cookie header" + then: "Response should contain cookie header" assert removeExpiresValue(response.headers[SET_COOKIE_HEADER]) == - "receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" + "receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" where: privacySandbox << [PrivacySandbox.defaultPrivacySandbox, PrivacySandbox.getDefaultPrivacySandbox(true, -PBSUtils.randomNumber)] @@ -2266,14 +2281,14 @@ class CookieSyncSpec extends BaseSpec { and: "Save account with cookie and privacySandbox configs" def accountAuctionConfig = new AccountAuctionConfig(privacySandbox: PrivacySandbox.getDefaultPrivacySandbox(true, null)) - def accountConfig = new AccountConfig(status: AccountStatus.ACTIVE, auction: accountAuctionConfig) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) def account = new Account(uuid: accountId, config: accountConfig) accountDao.save(account) when: "PBS processes cookie sync request" def response = prebidServerService.sendCookieSyncRequestRaw(cookieSyncRequest, uidsCookie) - then: "Response should contain cookie header" + then: "Response should contain cookie header" assert removeExpiresValue(response.headers[SET_COOKIE_HEADER]) == "receive-cookie-deprecation=1; Max-Age=${TimeUnit.DAYS.toSeconds(7)}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" } @@ -2298,7 +2313,7 @@ class CookieSyncSpec extends BaseSpec { when: "PBS processes cookie sync request" def response = pbsService.sendCookieSyncRequestRaw(cookieSyncRequest, uidsCookie) - then: "Response should contain cookie header" + then: "Response should contain cookie header" assert removeExpiresValue(response.headers[SET_COOKIE_HEADER]) == "receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned" } @@ -2315,20 +2330,20 @@ class CookieSyncSpec extends BaseSpec { when: "PBS processes cookie sync request" def response = prebidServerService.sendCookieSyncRequestRaw(cookieSyncRequest, uidsCookie) - then: "Response shouldn't contain cookie header" + then: "Response shouldn't contain cookie header" assert !response.headers[SET_COOKIE_HEADER] } private static Map getValidBidderUserSyncs(CookieSyncResponse cookieSyncResponse) { cookieSyncResponse.bidderStatus - .findAll { it.userSync } - .collectEntries { [it.bidder, it.userSync] } + .findAll { it.userSync } + .collectEntries { [it.bidder, it.userSync] } } private static Map getRejectedBidderUserSyncs(CookieSyncResponse cookieSyncResponse) { cookieSyncResponse.bidderStatus - .findAll { it.error } - .collectEntries { [it.bidder, it.error] } + .findAll { it.error } + .collectEntries { [it.bidder, it.error] } } private static String removeExpiresValue(String cookie) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy index ab22cb65cc2..df5bba70028 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -9,17 +9,23 @@ import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversio import java.math.RoundingMode +import static org.prebid.server.functional.model.Currency.CAD +import static org.prebid.server.functional.model.Currency.CHF import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer class CurrencySpec extends BaseSpec { private static final Currency DEFAULT_CURRENCY = USD private static final int PRICE_PRECISION = 3 - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.8872327211427558, - (JPY): 114.12], + private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(USD): 1, + (EUR): 0.9249838127832763, + (CHF): 0.9033391915641477, + (JPY): 151.1886041994265, + (CAD): 1.357136250115623], (EUR): [(USD): 1.3429368029739777]] private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) @@ -113,6 +119,77 @@ class CurrencySpec extends BaseSpec { JPY || USD } + def "PBS should use cross currency conversion when direct, reverse and intermediate conversion is not available"() { + given: "Default BidRequest with #requestCurrency currency" + def bidRequest = BidRequest.defaultBidRequest.tap { cur = [requestCurrency] } + + and: "Default Bid with a #bidCurrency currency" + def bidderResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = bidCurrency } + bidder.setResponse(bidRequest.id, bidderResponse) + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Auction response should contain bid in #requestCurrency currency" + assert bidResponse.cur == requestCurrency + def bidPrice = bidResponse.seatbid[0].bid[0].price + assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price + assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency + + where: + requestCurrency || bidCurrency + CHF || JPY + JPY || CHF + CAD || JPY + JPY || CAD + EUR || CHF + CHF || EUR + } + + def "PBS should emit warning when request contain more that one currency"() { + given: "Default BidRequest with currencies" + def currencies = [EUR, USD] + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = currencies + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain first requested currency" + assert bidResponse.cur == currencies[0] + + and: "Bidder request should contain requested currencies" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == currencies + + and: "Bid response should contain warnings" + assert bidResponse.ext.warnings[GENERIC]?.message == ["a single currency (${currencies[0]}) has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency." as String] + } + + def "PBS shouldn't emit warning when request contain one currency"() { + given: "Default BidRequest with currency" + def currency = [USD] + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = currency + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain first requested currency" + assert bidResponse.cur == currency[0] + + and: "Bidder request should contain requested currency" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == currency + + and: "Bid response shouldn't contain warnings" + assert !bidResponse.ext.warnings + } + private static Map getExternalCurrencyConverterConfig() { ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, "currency-converter.external-rates.enabled" : "true", @@ -129,11 +206,26 @@ class CurrencySpec extends BaseSpec { def conversionRate if (fromCurrency == toCurrency) { conversionRate = 1 - } else if (fromCurrency in DEFAULT_CURRENCY_RATES) { + } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else { + } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + } else { + conversionRate = getCrossConversionRate(fromCurrency, toCurrency) } conversionRate } + + private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { + for (Map rates : DEFAULT_CURRENCY_RATES.values()) { + def fromRate = rates?[fromCurrency] + def toRate = rates?[toCurrency] + + if (fromRate && toRate) { + return toRate / fromRate + } + } + + null + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy index e5df7fccc9c..3f7682eb708 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy @@ -105,7 +105,6 @@ class DebugSpec extends BaseSpec { bidRequest.ext.prebid.debug = 1 and: "Account in the DB" - def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) accountDao.save(account) @@ -121,6 +120,10 @@ class DebugSpec extends BaseSpec { //TODO possibly change message after clarifications assert response.ext?.warnings[ErrorType.PREBID]?.collect { it.message } == ["Debug turned off for account"] + + where: + accountConfig << [new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)), + new AccountConfig(auction: new AccountAuctionConfig(debugAllowSnakeCase: false))] } def "PBS should not return debug information when bidder-level setting debug.allowed = false is overridden by account-level setting debug-allowed = true"() { @@ -132,7 +135,6 @@ class DebugSpec extends BaseSpec { bidRequest.ext.prebid.debug = 1 and: "Account in the DB" - def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: true)) def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) accountDao.save(account) @@ -147,6 +149,10 @@ class DebugSpec extends BaseSpec { assert response.ext?.warnings[ErrorType.PREBID]?.collect { it.code } == [999] assert response.ext?.warnings[ErrorType.PREBID]?.collect { it.message } == ["Debug turned off for bidder: $GENERIC.value" as String] + + where: + accountConfig << [new AccountConfig(auction: new AccountAuctionConfig(debugAllow: true)), + new AccountConfig(auction: new AccountAuctionConfig(debugAllowSnakeCase: true))] } def "PBS should not return debug information when bidder-level setting debug.allowed = true is overridden by account-level setting debug-allowed = false"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy new file mode 100644 index 00000000000..529f9c9c961 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy @@ -0,0 +1,275 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Eid +import org.prebid.server.functional.model.request.auction.EidPermission +import org.prebid.server.functional.model.request.auction.ExtRequestPrebidData +import org.prebid.server.functional.model.request.auction.Uid +import org.prebid.server.functional.model.request.auction.UidExt +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class EidsSpec extends BaseSpec { + + private static final String EMPTY_STRING = "" + + def "PBS shouldn't populate user.id from user.ext data"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: PBSUtils.randomString, + uids: [new Uid(id: PBSUtils.randomString, ext: new UidExt(stype: PBSUtils.randomString))])])) + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain user.id" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.id + } + + def "PBS should send same eids as in original request"() { + given: "Default basic BidRequest with generic bidder" + def eids = [new Eid(source: PBSUtils.randomString, uids: [new Uid(id: PBSUtils.randomString)])] + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: eids) + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == eids + } + + def "PBS eids should be passed only to permitted bidders"() { + given: "Default bid request with generic bidder and eids" + def eids = [new Eid(source: PBSUtils.randomString, uids: [new Uid(id: PBSUtils.randomString)])] + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: eids) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: + [new EidPermission(source: PBSUtils.randomString, bidders: [eidsBidder])]) + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == eids + + where: + eidsBidder << [WILDCARD, GENERIC] + } + + def "PBS eids shouldn't be passed to restricted bidders"() { + given: "Default bid request with generic bidder" + def sourceId = PBSUtils.randomString + def eids = [new Eid(source: sourceId, uids: [new Uid(id: sourceId)])] + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: eids) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [new EidPermission(source: sourceId, bidders: [OPENX])]) + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + } + + def "PBs eid permissions should affect only specified on source"() { + given: "PBs with openx bidder" + def pbsService = pbsServiceFactory.getService( + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + + and: "Default bid request with eidpremissions and openx bidder" + def eidSource = PBSUtils.randomString + def openxEid = new Eid(source: eidSource, uids: [new Uid(id: PBSUtils.randomString)]) + def genericEid = new Eid(source: PBSUtils.randomString, uids: [new Uid(id: PBSUtils.randomString)]) + def eids = [openxEid, genericEid] + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: eids) + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [new EidPermission(source: eidSource, bidders: [OPENX])]) + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain two bidder request" + def bidderRequests = getRequests(response) + assert bidderRequests.size() == 2 + + and: "Generic bidder should contain one eid" + assert bidderRequests[GENERIC.value].user.eids.sort().first == [genericEid] + + and: "Openx bidder should contain two eids" + assert bidderRequests[OPENX.value].user.eids.sort().last.sort() == eids.sort() + } + + def "PBs eid permissions for non existing source should not stop auction"() { + given: "PBs with openx bidder" + def pbsService = pbsServiceFactory.getService( + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + + and: "Default bid request with eidpremissions and openx bidder" + def firstEid = new Eid(source: PBSUtils.randomString, uids: [new Uid(id: PBSUtils.randomString)]) + def secondEid = new Eid(source: PBSUtils.randomString, uids: [new Uid(id: PBSUtils.randomString)]) + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [firstEid, secondEid]) + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.data = new ExtRequestPrebidData( + eidpermissions: [new EidPermission(source: PBSUtils.randomString, bidders: [OPENX])]) + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain two bidder request" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert bidderRequests.size() == 2 + + and: "Openx and Generic bidder should contain two eid" + bidderRequests.user.eids.each { + assert it.sort() == [secondEid, firstEid].sort() + } + } + + def "PBs missing bidders in eid permissions should throw an error"() { + given: "Default request with eidpremissions and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [new Eid(source: PBSUtils.randomString, uids: [new Uid(id: PBSUtils.randomString)])]) + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.data = new ExtRequestPrebidData( + eidpermissions: [new EidPermission(source: PBSUtils.randomString, bidders: eidsBidder), + new EidPermission(source: PBSUtils.randomString)]) + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw error" + def exception = thrown(PrebidServerException) + assert exception.responseBody == "Invalid request format: request.ext.prebid.data.eidpermissions[].bidders[] " + + "required values but was empty or null" + + where: + eidsBidder << [[WILDCARD], [], null] + } + + def "PBs eid permissions should honor bidder alias"() { + given: "Default request with eidpremissions and openx bidder" + def sourceId = PBSUtils.randomString + def eid = new Eid(source: sourceId, uids: [new Uid(id: PBSUtils.randomString)]) + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [eid]) + imp[0].ext.prebid.bidder.alias = new Generic() + ext.prebid.tap { + data = new ExtRequestPrebidData(eidpermissions: [new EidPermission(source: sourceId, bidders: [ALIAS])]) + aliases = [(ALIAS.value): GENERIC] + } + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain two bidder request" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + def sortedEids = bidderRequests.user.sort { it.eids } + assert bidderRequests.size() == 2 + + and: "Generic bidder shouldn't contain eids" + assert !sortedEids[0].eids + + and: "Alias bidder should contain one eids" + assert sortedEids[1].eids == [eid] + } + + def "PBS should populate warning for one removed UID when invalid uidId"() { + given: "BidRequest with eids" + def sourceId = PBSUtils.randomString + def validUidId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: sourceId, + uids: [new Uid(id: invalidUidId), + new Uid(id: validUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids.uids.id.flatten() == [validUidId] + + and: "Bid response should contain warning" + assert bidResponse.ext.warnings[PREBID]?.code == [999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["removed EID ${sourceId} due to empty ID" as String] + + where: + invalidUidId << [EMPTY_STRING, null] + } + + def "PBS should populate warnings for removed UIDs and entire eids when requested invalid uidIds"() { + given: "BidRequest with eids" + def sourceId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: sourceId, + uids: [new Uid(id: invalidUidId), + new Uid(id: invalidUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + + and: "Bid response should contain warnings" + assert bidResponse.ext.warnings[PREBID]?.code == [999, 999, 999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["removed EID ${sourceId} due to empty ID" as String, + "removed EID ${sourceId} due to empty ID" as String, + "removed empty EID array" as String] + + where: + invalidUidId << [EMPTY_STRING, null] + } + + def "PBS shouldn't populate warning for UID when Uid id is valid"() { + given: "BidRequest with eids" + def validUidId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: PBSUtils.randomString, + uids: [new Uid(id: validUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids.uids.id.flatten() == [validUidId] + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy index c105aaef867..1d36b8beadc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy @@ -1,16 +1,18 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Audio import org.prebid.server.functional.model.request.auction.Banner import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.BidderControls import org.prebid.server.functional.model.request.auction.GenericPreferredBidder import org.prebid.server.functional.model.request.auction.Native -import org.prebid.server.functional.model.request.auction.BidderControls +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER @@ -52,7 +54,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -62,6 +64,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with one requested preferred media type when default adapters multi format is false in config and preferred media type specified at account level"() { @@ -98,7 +106,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -108,6 +116,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with all requested media type when multi format is true in config and preferred media type specified at request level"() { @@ -119,7 +133,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -129,6 +143,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp.banner assert bidderRequest.imp.audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with all requested media type when multi format is true in config and preferred media type specified at account level"() { @@ -190,7 +210,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -200,6 +220,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with warning and don't make a bidder call when multi format at request and preferred media type specified at account level with non requested media type"() { @@ -241,7 +267,7 @@ class FilterMultiFormatSpec extends BaseSpec { imp[0].banner = null imp[0].audio = Audio.defaultAudio imp[0].nativeObj = Native.defaultNative - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -254,6 +280,12 @@ class FilterMultiFormatSpec extends BaseSpec { assert bidResponse.ext.warnings[GENERIC]?.message == ["Imp ${bidRequest.imp[0].id} does not have a media type after filtering and has been removed from the request for this bidder.", "Bid request contains 0 impressions after filtering."] + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and preferred media type specified at account level"() { @@ -292,7 +324,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = null imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -304,6 +336,12 @@ class FilterMultiFormatSpec extends BaseSpec { and: "Bid response shouldn't contain warning" assert !bidResponse.ext.warnings + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and multi format is false and preferred media type specified at request level with null"() { @@ -315,7 +353,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.getDefaultBanner() imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: NULL)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -326,6 +364,12 @@ class FilterMultiFormatSpec extends BaseSpec { and: "Bid response shouldn't contain warning" assert !bidResponse.ext?.warnings + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: NULL)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: NULL)), + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and multi format is false and preferred media type specified at account level with null"() { @@ -364,7 +408,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } and: "Account in the DB with preferred media type" @@ -379,5 +423,53 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] + } + + def "PBS should not preferred media type specified at request level when it's alias bidder"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapter-defaults.ortb.multiformat-supported": "false", + "adapters.generic.ortb.multiformat-supported": "false") + + and: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = Banner.defaultBanner + audio = Audio.defaultAudio + ext.prebid.bidder.tap { + alias = new Generic() + generic = null + } + } + ext.prebid.tap { + it.aliases = [(ALIAS.value): BidderName.GENERIC] + it.bidderControls = bidderControls + } + } + + and: "Account in the DB with preferred media type" + def accountConfig = new AccountAuctionConfig(preferredMediaType: [(BidderName.GENERIC): AUDIO]) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain preferred media type from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.imp[0].banner + assert bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/GeoSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/GeoSpec.groovy new file mode 100644 index 00000000000..3d19d9d878d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/GeoSpec.groovy @@ -0,0 +1,499 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountSetting +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.pricefloors.Country.CAN +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.CAN_IP +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.USA_IP +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.util.privacy.model.State.ALABAMA +import static org.prebid.server.functional.util.privacy.model.State.ONTARIO +import static org.prebid.server.functional.util.privacy.model.State.QUEBEC + +class GeoSpec extends BaseSpec { + + private static final String GEO_LOCATION_REQUESTS = "geolocation_requests" + private static final String GEO_LOCATION_FAIL = "geolocation_fail" + private static final String GEO_LOCATION_SUCCESSFUL = "geolocation_successful" + private static final Map GEO_LOCATION = ["geolocation.type" : "configuration", + "geolocation.configurations.[0].address-pattern" : USA_IP.v4, + "geolocation.configurations.[0].geo-info.country": USA.ISOAlpha2, + "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation, + "geolocation.configurations.[1].address-pattern" : CAN_IP.v4, + "geolocation.configurations.[1].geo-info.country": CAN.ISOAlpha2, + "geolocation.configurations.[1].geo-info.region" : QUEBEC.abbreviation] + + def "PBS should populate geo with country and region and take precedence from device.id when geo location enabled in host and account config and ip specified in both places"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = settingDefaultAccountGeoLookup + } + def defaultPbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(config), + "geolocation.enabled" : "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: settingAccountDefaultAccountGeoLookup) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": CAN_IP.v4]) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == USA + assert bidderRequests.device.geo.region == ALABAMA.abbreviation + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 + assert !metrics[GEO_LOCATION_FAIL] + + where: + settingDefaultAccountGeoLookup | settingAccountDefaultAccountGeoLookup + new AccountSetting(geoLookupSnakeCase: false) | new AccountSetting(geoLookupSnakeCase: true) + new AccountSetting(geoLookup: true) | new AccountSetting(geoLookup: true) + new AccountSetting(geoLookupSnakeCase: true) | new AccountSetting(geoLookupSnakeCase: true) + new AccountSetting(geoLookup: true) | new AccountSetting(geoLookup: null) + new AccountSetting(geoLookupSnakeCase: true) | new AccountSetting(geoLookupSnakeCase: null) + } + + def "PBS should populate geo with country and region when geo location enabled in host and account config and ip present in device.id"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookup) + } + def defaultPbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(config), + "geolocation.enabled" : "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == USA + assert bidderRequests.device.geo.region == ALABAMA.abbreviation + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookup | accountGeoLookup + false | true + true | true + true | null + } + + def "PBS should populate geo with country and region when geo location enabled in host and account config and ip present in header"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookup) + } + def defaultPbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(config), + "geolocation.enabled" : "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == USA + assert bidderRequests.device.geo.region == ALABAMA.abbreviation + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookup | accountGeoLookup + false | true + true | true + true | null + } + + def "PBS shouldn't populate geo with country and region when geo location disable in host and account config enabled and ip present in device.ip"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookupConfig) + } + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["settings.default-account-config": encode(config), + "geolocation.enabled" : hostGeolocation]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookupConfig | hostGeolocation | accountGeoLookup + true | "true" | false + true | "false" | true + false | "false" | false + false | "true" | false + } + + def "PBS shouldn't populate geo with country and region when geo location disable in host and account config enabled and ip present in header"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookupConfig) + } + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["settings.default-account-config": encode(config), + "geolocation.enabled" : hostGeolocation]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookupConfig | hostGeolocation | accountGeoLookup + true | "true" | false + true | "false" | true + false | "false" | false + false | "true" | false + } + + def "PBS shouldn't populate geo with country, region and emit error in log and metric when geo look up failed and ip present in device.id"() { + given: "Test start time" + def startTime = Instant.now() + + and: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["geolocation.configurations.[0].address-pattern": PBSUtils.randomNumber as String, + "geolocation.enabled" : "true"]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_FAIL] == 1 + assert !metrics[GEO_LOCATION_SUCCESSFUL] + + and: "PBs should emit geo failed logs" + def logs = defaultPbsService.getLogsByTime(startTime) + def getLocation = getLogsByText(logs, "GeoLocationServiceWrapper") + assert getLocation.size() == 1 + assert getLocation[0].contains("Geolocation lookup failed: " + + "ConfigurationGeoLocationService: Geo location lookup failed.") + } + + def "PBS shouldn't populate geo with country, region and emit error in log and metric when geo look up failed and ip present in header"() { + given: "Test start time" + def startTime = Instant.now() + + and: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["geolocation.configurations.[0].address-pattern": PBSUtils.randomNumber as String, + "geolocation.enabled" : "true"]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_FAIL] == 1 + assert !metrics[GEO_LOCATION_SUCCESSFUL] + + and: "PBs should emit geo failed logs" + def logs = defaultPbsService.getLogsByTime(startTime) + def getLocation = getLogsByText(logs, "GeoLocationServiceWrapper") + assert getLocation.size() == 1 + assert getLocation[0].contains("Geolocation lookup failed: " + + "ConfigurationGeoLocationService: Geo location lookup failed.") + } + + def "PBS shouldn't populate country and region via geo when geo enabled in account and country and region specified in request and ip present in device.id"() { + given: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService( + ["geolocation.enabled": "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: CAN, + region: ONTARIO.abbreviation, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == CAN + assert bidderRequests.device.geo.region == ONTARIO.abbreviation + + and: "Metrics processed across activities shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + } + + def "PBS shouldn't populate country and region via geo when geo enabled in account and country and region specified in request and ip present in header"() { + given: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService( + ["geolocation.enabled": "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: CAN, + region: ONTARIO.abbreviation, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == CAN + assert bidderRequests.device.geo.region == ONTARIO.abbreviation + + and: "Metrics processed across activities shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy index 99cd8831745..caabebba8ff 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy @@ -32,7 +32,7 @@ class HttpSettingsSpec extends BaseSpec { def "PBS should take account information from http data source on auction request"() { given: "Get basic BidRequest with generic bidder and set gdpr = 1" def bidRequest = BidRequest.defaultBidRequest - bidRequest.regs.ext.gdpr = 1 + bidRequest.regs.gdpr = 1 and: "Prepare default account response with gdpr = 0" def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest?.site?.publisher?.id) @@ -61,7 +61,7 @@ class HttpSettingsSpec extends BaseSpec { and: "Get basic stored request and set gdpr = 1" def ampStoredRequest = BidRequest.defaultBidRequest ampStoredRequest.site.publisher.id = ampRequest.account - ampStoredRequest.regs.ext.gdpr = 1 + ampStoredRequest.regs.gdpr = 1 and: "Save storedRequest into DB" def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) diff --git a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy new file mode 100644 index 00000000000..085bd19a690 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy @@ -0,0 +1,295 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Pmp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.EMPTY +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.RUBICON +import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class ImpRequestSpec extends BaseSpec { + + private final PrebidServerService defaultPbsServiceWithAlias = pbsServiceFactory.getService(GENERIC_ALIAS_CONFIG) + private static final String EMPTY_ID = "" + + def "PBS should update imp fields when imp.ext.prebid.imp contain bidder information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = Pmp.defaultPmp + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + ext.prebid.imp = [(bidderName): new Imp(pmp: extPmp)] + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [extPmp] + + and: "BidderRequest should contain original stored request id" + assert bidderRequest.imp.ext.prebid.storedRequest.id == [storedRequestId] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert bidderRequest?.imp?.ext?.prebid?.imp == [null] + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should update only required imp when it contain bidder information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def impWithParameters = Imp.defaultImpression.tap { + pmp = Pmp.defaultPmp + ext.prebid.imp = [(bidderName): new Imp(pmp: extPmp)] + } + def impWithoutParameters = Imp.defaultImpression.tap { + pmp = Pmp.defaultPmp + } + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [impWithParameters, impWithoutParameters] + } + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value only for required imp" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.find { it.id == impWithParameters.id }?.pmp == extPmp + assert bidderRequest.imp.find { it.id == impWithoutParameters.id }?.pmp == impWithoutParameters.pmp + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should update imp fields when imp.ext.prebid.imp contain bidder alias information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = Pmp.defaultPmp + ext.prebid.imp = [(aliasName): new Imp(pmp: extPmp)] + } + ext.prebid.aliases = [(aliasName.value): GENERIC] + } + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [extPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + aliasName | bidderName + ALIAS | GENERIC + ALIAS_CAMEL_CASE | GENERIC + ALIAS | GENERIC_CAMEL_CASE + ALIAS_CAMEL_CASE | GENERIC_CAMEL_CASE + } + + def "PBS shouldn't update imp fields when imp.ext.prebid.imp contain only bidder with invalid name"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(bidderName): new Imp(pmp: Pmp.defaultPmp)] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["WARNING: request.imp[0].ext.prebid.imp.${bidderName} was dropped with the reason: invalid bidder"] + + and: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [WILDCARD, UNKNOWN] + } + + def "PBS shouldn't update imp fields and without warning when imp.ext.prebid.imp contain not applicable bidder"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(RUBICON): new Imp(pmp: Pmp.defaultPmp)] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain warning" + assert !response?.ext?.warnings + + and: "BidderRequest should contain pmp from original imp" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + } + + def "PBS should always update specified bidder imp when imp.ext.prebid.imp contain such bidder"() { + given: "PBs with openx bidder" + def pbsService = pbsServiceFactory.getService( + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + + and: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def extPrebidImpPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.imp = [(OPENX): new Imp(pmp: extPrebidImpPmp)] + } + } + + when: "Requesting PBS auction" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain warning" + assert !response?.ext?.warnings + + and: "Generic bidderRequest should contain pmp from original imp" + def bidderToBidderRequests = getRequests(response) + assert bidderToBidderRequests[GENERIC.value].first.imp.pmp == [impPmp] + + and: "OpenX bidderRequest should contain pmp from ext.prebid.imp" + assert bidderToBidderRequests[OPENX.value].first.imp.pmp == [extPrebidImpPmp] + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequests" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert !bidderRequests?.imp?.ext?.prebid?.imp?.flatten() + } + + def "PBS should validate imp and add proper warning when imp.ext.prebid.imp contain invalid ortb data"() { + given: "BidRequest with invalid config for ext.prebid.imp" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(GENERIC): Imp.defaultImpression.tap { + id = EMPTY_ID + }] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["imp.ext.prebid.imp.generic can not be merged into original imp [id=${bidRequest.imp.first.id}], " + + "reason: imp[id=] missing required field: \"id\""] + + and: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + } + + def "PBS shouldn't update imp fields when imp.ext.prebid.imp contain invalid empty data"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = prebidImp + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "BidderRequest should contain original stored request id" + assert bidderRequest.imp.ext.prebid.storedRequest.id == [storedRequestId] + + and: "PBS should remove imp.ext.prebid.imp.pmp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp?.get(GENERIC)?.pmp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + prebidImp << [ + null, + [:], + [(EMPTY): new Imp(pmp: Pmp.defaultPmp)], + [(GENERIC): null], + [(GENERIC): new Imp()], + [(GENERIC): new Imp(pmp: new Pmp())] + ] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy index ae3795ca1f8..92ef175681e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/MetricsSpec.groovy @@ -7,6 +7,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Dooh import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerService import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.BASIC import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED @@ -16,8 +17,11 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha class MetricsSpec extends BaseSpec { + private final PrebidServerService softPrebidService = pbsServiceFactory.getService(['auction.strict-app-site-dooh': 'false']) + def setup() { flushMetrics(defaultPbsService) + flushMetrics(softPrebidService) } def "PBS should not populate account metric when verbosity level is none"() { @@ -44,7 +48,6 @@ class MetricsSpec extends BaseSpec { and: "Account in the DB" def accountId = bidRequest.site.publisher.id - def accountMetricsConfig = new AccountConfig(metrics: new AccountMetricsConfig(verbosityLevel: BASIC)) def account = new Account(uuid: accountId, config: accountMetricsConfig) accountDao.save(account) @@ -58,6 +61,10 @@ class MetricsSpec extends BaseSpec { and: "account..generic and requests.type.openrtb2-web metrics shouldn't populated" assert !metrics.findAll({ it.key.startsWith("account.${accountId}.generic") }) assert !metrics["account.${accountId}.requests.type.openrtb2-web" as String] + + where: + accountMetricsConfig << [new AccountConfig(metrics: new AccountMetricsConfig(verbosityLevel: BASIC)), + new AccountConfig(metrics: new AccountMetricsConfig(verbosityLevelSnakeCase: BASIC))] } def "PBS should update account..* metrics when verbosity level is detailed"() { @@ -113,7 +120,7 @@ class MetricsSpec extends BaseSpec { assert !metrics["account.${accountId}.requests.type.openrtb2-app" as String] } - def "PBS should ignore site distribution channel and update only dooh metrics when presented dooh and site in request"() { + def "PBS with soft setup should ignore site distribution channel and update only dooh metrics when presented dooh and site in request"() { given: "Default bid request with dooh and site" def bidRequest = BidRequest.defaultBidRequest.tap { dooh = Dooh.defaultDooh @@ -126,7 +133,7 @@ class MetricsSpec extends BaseSpec { accountDao.save(account) when: "Requesting PBS auction" - defaultPbsService.sendAuctionRequest(bidRequest) + softPrebidService.sendAuctionRequest(bidRequest) then: "Bidder request should have only dooh data" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -134,7 +141,7 @@ class MetricsSpec extends BaseSpec { assert !bidderRequest.site and: "Metrics processed across site should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() + def metrics = softPrebidService.sendCollectedMetricsRequest() assert metrics["account.${accountId}.requests.type.openrtb2-dooh" as String] == 1 assert metrics["adapter.generic.requests.type.openrtb2-dooh" as String] == 1 @@ -146,7 +153,7 @@ class MetricsSpec extends BaseSpec { assert !metrics["account.${accountId}.requests.type.openrtb2-app" as String] } - def "PBS should ignore other distribution channel and update only app metrics when presented app ant other channels in request"() { + def "PBS with soft setup should ignore other distribution channel and update only app metrics when presented app ant other channels in request"() { given: "Account in the DB" def accountId = bidRequest.app.publisher.id def accountMetricsConfig = new AccountConfig(metrics: new AccountMetricsConfig(verbosityLevel: DETAILED)) @@ -154,7 +161,7 @@ class MetricsSpec extends BaseSpec { accountDao.save(account) when: "Requesting PBS auction" - defaultPbsService.sendAuctionRequest(bidRequest) + softPrebidService.sendAuctionRequest(bidRequest) then: "Bidder request should have only site data" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -163,7 +170,7 @@ class MetricsSpec extends BaseSpec { assert !bidderRequest.dooh and: "Metrics processed across site should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() + def metrics = softPrebidService.sendCollectedMetricsRequest() assert metrics["account.${accountId}.requests.type.openrtb2-app" as String] == 1 assert metrics["adapter.generic.requests.type.openrtb2-app" as String] == 1 diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index cc6832c479d..21bb80df135 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.auction.Audio import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Content @@ -8,6 +9,7 @@ import org.prebid.server.functional.model.request.auction.Dooh import org.prebid.server.functional.model.request.auction.DoohExt import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Network +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest import org.prebid.server.functional.model.request.auction.Producer import org.prebid.server.functional.model.request.auction.Publisher import org.prebid.server.functional.model.request.auction.Qty @@ -15,6 +17,7 @@ import org.prebid.server.functional.model.request.auction.RefSettings import org.prebid.server.functional.model.request.auction.RefType import org.prebid.server.functional.model.request.auction.Refresh import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Source import org.prebid.server.functional.model.request.auction.SourceType import org.prebid.server.functional.model.request.auction.User @@ -39,22 +42,21 @@ class OrtbConverterSpec extends BaseSpec { @Shared PrebidServerService prebidServerServiceWithElderOrtb = pbsServiceFactory.getService([(ORTB_PROPERTY_VERSION): "2.5"]) - def "PBS shouldn't move regs to past location when adapter support ortb 2.6"() { + def "PBS shouldn't move regs.{gdpr,usPrivacy} to regs.ext.{gdpr,usPrivacy} when adapter support ortb 2.6"() { given: "Default bid request with regs object" def usPrivacyRandomString = PBSUtils.randomString + def gdpr = 0 def bidRequest = BidRequest.defaultBidRequest.tap { - regs = Regs.defaultRegs.tap { - usPrivacy = usPrivacyRandomString - } + regs = new Regs(usPrivacy: usPrivacyRandomString, gdpr: gdpr) } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same regs as on request" + then: "Bidder request should contain the same regs.{gdpr,usPrivacy} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { regs.usPrivacy == usPrivacyRandomString - regs.gdpr == 0 + regs.gdpr == gdpr !regs.ext } } @@ -71,14 +73,14 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same imp.rwdd as on request" + then: "Bidder request should contain the same imp.rwdd as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { imp.first().rwdd == rwdRandomNumber !imp.first().ext.prebid } } - def "PBS shouldn't move eids to past location when adapter support ortb 2.6"() { + def "PBS shouldn't move user.eids to user.ext.eids when adapter support ortb 2.6"() { given: "Default bid request with user.eids" def defaultEids = [Eid.defaultEid] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -90,16 +92,14 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same user.eids as on request" + then: "Bidder request should contain user.eids as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - user.eids.first().source == defaultEids.first().source - user.eids.first().uids.first().id == defaultEids.first().uids.first().id - user.eids.first().uids.first().atype == defaultEids.first().uids.first().atype + user.eids == defaultEids !user.ext } } - def "PBS shouldn't move consent to past location when adapter support ortb 2.6"() { + def "PBS shouldn't move consent to user.ext.consent when adapter support ortb 2.6"() { given: "Default bid request with user.consent" def consentRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -111,14 +111,14 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same user.consent as on request" + then: "Bidder request should contain the same user.consent as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { user.consent == consentRandomString !user.ext } } - def "PBS shouldn't move schain to past location when adapter support ortb 2.6"() { + def "PBS shouldn't move source.schain to source.ext.schain when adapter support ortb 2.6"() { given: "Default bid request with source.schain" def defaultSource = Source.defaultSource def defaultSupplyChain = defaultSource.schain @@ -129,16 +129,9 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same source.schain as on request" + then: "Bidder request should contain the same source.schain as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - source.schain.ver == defaultSupplyChain.ver - source.schain.complete == defaultSupplyChain.complete - source.schain.nodes.first().asi == defaultSupplyChain.nodes.first().asi - source.schain.nodes.first().sid == defaultSupplyChain.nodes.first().sid - source.schain.nodes.first().rid == defaultSupplyChain.nodes.first().rid - source.schain.nodes.first().name == defaultSupplyChain.nodes.first().name - source.schain.nodes.first().domain == defaultSupplyChain.nodes.first().domain - source.schain.nodes.first().hp == defaultSupplyChain.nodes.first().hp + source.schain == defaultSupplyChain !source.ext } } @@ -154,7 +147,7 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same source.schain as on request but should be in source.ext.schain" + then: "Bidder request should contain the same source.schain as on request but should be in source.ext.schain" verifyAll(bidder.getBidderRequest(bidRequest.id)) { source.ext.schain.ver == defaultSupplyChain.ver source.ext.schain.complete == defaultSupplyChain.complete @@ -168,7 +161,7 @@ class OrtbConverterSpec extends BaseSpec { } } - def "PBS should move consent to past location when adapter doesn't support ortb 2.6"() { + def "PBS should move consent to user.ext.consent when adapter doesn't support ortb 2.6"() { given: "Default bid request with user.consent" def consentRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -180,14 +173,14 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same user.consent as on request but should be in user.ext" + then: "Bidder request should contain the same user.consent as on request but should be in user.ext" verifyAll(bidder.getBidderRequest(bidRequest.id)) { user.ext.consent == consentRandomString !user.consent } } - def "PBS should move eids to past location when adapter doesn't support ortb 2.6"() { + def "PBS should move eids to user.ext.eids when adapter doesn't support ortb 2.6"() { given: "Default bid request with user.eids" def defaultEids = [Eid.defaultEid] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -199,7 +192,7 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same user.eids as on request but should be in user.ext.eids" + then: "Bidder request should contain the same user.eids as on request but should be in user.ext.eids" verifyAll(bidder.getBidderRequest(bidRequest.id)) { user.ext.eids.first().source == defaultEids.first().source user.ext.eids.first().uids.first().id == defaultEids.first().uids.first().id @@ -208,7 +201,7 @@ class OrtbConverterSpec extends BaseSpec { } } - def "PBS should move regs to past location when adapter doesn't support ortb 2.6"() { + def "PBS should move regs to regs.ext.{gdpr,upPrivacy} when adapter doesn't support ortb 2.6"() { given: "Default bid request with regs object" def usPrivacyRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -220,7 +213,7 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same regs object as on request but should be in regs.ext" + then: "Bidder request should contain the same regs object as on request but should be in regs.ext" verifyAll(bidder.getBidderRequest(bidRequest.id)) { regs.ext.usPrivacy == usPrivacyRandomString regs.ext.gdpr == 0 @@ -229,26 +222,24 @@ class OrtbConverterSpec extends BaseSpec { } } - def "PBS should move rewarded video to past location when adapter doesn't support ortb 2.6"() { + def "PBS should copy rewarded video to imp.ext.prebid.isRewardedInventory when adapter support ortb 2.6"() { given: "Default bid request with rwdd" def rwdRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].tap { - rwdd = rwdRandomNumber - } + imp[0].rwdd = rwdRandomNumber } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same imp.rwdd as on request but should be in ext.prebid" + then: "Bidder request should contain the same imp.rwdd as on request but should be also in ext.prebid" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp.first().ext.prebid.isRewardedInventory == rwdRandomNumber - !imp.first().rwdd + imp[0].ext.prebid.isRewardedInventory == rwdRandomNumber + imp[0].rwdd == rwdRandomNumber } } - def "PBS shouldn't remove wlangb when we we support ortb 2.6"() { + def "PBS shouldn't remove wlangb when bidder supports ortb 2.6"() { given: "Default bid request with wlangb" def wlangbRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -258,45 +249,43 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn contain the wlangb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - wlangb == wlangbRandomStrings - } + then: "Bidder request shouldn contain the wlangb as on request" + assert bidder.getBidderRequest(bidRequest.id).wlangb == wlangbRandomStrings } - def "PBS should remove wlangb when we don't support ortb 2.6"() { + def "PBS shouldn't remove wlangb when bidder doesn't support ortb 2.6"() { given: "Default bid request with wlangb" + def wlangbRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { - wlangb = [PBSUtils.randomString] + wlangb = wlangbRandomStrings } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the wlangb as on request" + then: "Bidder request should contain the wlangb as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !wlangb + wlangb == wlangbRandomStrings } } - def "PBS should remove device.langb when we don't support ortb 2.6"() { + def "PBS shouldn't remove device.langb when bidder doesn't support ortb 2.6"() { given: "Default bid request with device.langb" + def langbRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { device = new Device().tap { - langb = PBSUtils.randomString + langb = langbRandomString } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the device.langb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !device.langb - } + then: "Bidder request should contain the device.langb as on request" + assert bidder.getBidderRequest(bidRequest.id).device.langb == langbRandomString } - def "PBS shouldn't remove device.langb when we support ortb 2.6"() { + def "PBS shouldn't remove device.langb when bidder supports ortb 2.6"() { given: "Default bid request with device.langb" def langbRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -308,30 +297,27 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the device.langb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - device.langb == langbRandomString - } + then: "Bidder request should contain the device.langb as on request" + assert bidder.getBidderRequest(bidRequest.id).device.langb == langbRandomString } - def "PBS should remove site.content.langb when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.content.langb when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.content.langb" + def langbRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { site.content = Content.defaultContent.tap { - langb = PBSUtils.randomString + langb = langbRandomString } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.content.langb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.content.langb - } + then: "Bidder request should contain the site.content.langb as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.langb == langbRandomString } - def "PBS shouldn't remove site.content.langb when we support ortb 2.6"() { + def "PBS shouldn't remove site.content.langb when bidder supports ortb 2.6"() { given: "Default bid request with site.content.langb" def langbRandomString = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -343,30 +329,27 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.content.langb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.content.langb == langbRandomString - } + then: "Bidder request should contain the site.content.langb as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.langb == langbRandomString } - def "PBS should remove app.content.langb when we don't support ortb 2.6"() { + def "PBS shouldn't remove app.content.langb when bidder doesn't support ortb 2.6"() { given: "Default bid request with app.content.langb" + def langbRandomString = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { app.content = Content.defaultContent.tap { - langb = PBSUtils.randomString + langb = langbRandomString } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the app.content.langb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !app.content.langb - } + then: "Bidder request should contain the app.content.langb as on request" + assert bidder.getBidderRequest(bidRequest.id).app.content.langb == langbRandomString } - def "PBS shouldn't remove app.content.langb when we support ortb 2.6"() { + def "PBS shouldn't remove app.content.langb when bidder supports ortb 2.6"() { given: "Default bid request with app.content.langb" def langbRandomString = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { @@ -378,30 +361,27 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the app.content.langb as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - app.content.langb == langbRandomString - } + then: "Bidder request should contain the app.content.langb as on request" + assert bidder.getBidderRequest(bidRequest.id).app.content.langb == langbRandomString } - def "PBS should remove site.publisher.cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.publisher.cattax when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.publisher.cattax" + def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { site.publisher = Publisher.defaultPublisher.tap { - cattax = PBSUtils.randomNumber + cattax = cattaxRandomNumber } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.publisher.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.publisher.cattax - } + then: "Bidder request should contain the site.publisher.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.publisher.cattax == cattaxRandomNumber } - def "PBS shouldn't remove site.publisher.cattax when we support ortb 2.6"() { + def "PBS shouldn't remove site.publisher.cattax when bidder supports ortb 2.6"() { given: "Default bid request with site.publisher.cattax" def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { @@ -413,18 +393,17 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.publisher.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.publisher.cattax == cattaxRandomNumber - } + then: "Bidder request should contain the site.publisher.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.publisher.cattax == cattaxRandomNumber } - def "PBS should remove site.content.producer.cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.content.producer.cattax when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.content.producer.cattax" + def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { site.content = Content.defaultContent.tap { producer = Producer.defaultProducer.tap { - cattax = PBSUtils.randomNumber + cattax = cattaxRandomNumber } } } @@ -432,13 +411,11 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.content.producer.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.content.producer.cattax - } + then: "Bidder request should contain the site.content.producer.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.producer.cattax == cattaxRandomNumber } - def "PBS shouldn't remove site.content.producer.cattax when we support ortb 2.6"() { + def "PBS shouldn't remove site.content.producer.cattax when bidder supports ortb 2.6"() { given: "Default bid request with site.content.producer.cattax" def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { @@ -452,28 +429,25 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.content.producer.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.content.producer.cattax == cattaxRandomNumber - } + then: "Bidder request should contain the site.content.producer.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.producer.cattax == cattaxRandomNumber } - def "PBS should remove app.cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove app.cattax when bidder doesn't support ortb 2.6"() { given: "Default bid request with app.cattax" + def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - app.catTax = PBSUtils.randomNumber + app.catTax = cattaxRandomNumber } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the app.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !app.catTax - } + then: "Bidder request should contain the app.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).app.catTax == cattaxRandomNumber } - def "PBS shouldn't remove app.cattax when we support ortb 2.6"() { + def "PBS shouldn't remove app.cattax when bidder supports ortb 2.6"() { given: "Default bid request with app.cattax" def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { @@ -483,28 +457,25 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the app.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - app.catTax == cattaxRandomNumber - } + then: "Bidder request should contain the app.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).app.catTax == cattaxRandomNumber } - def "PBS should remove cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove cattax when bidder doesn't support ortb 2.6"() { given: "Default bid request with cattax" + def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - cattax = PBSUtils.randomNumber + cattax = cattaxRandomNumber } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !cattax - } + then: "Bidder request should contain the cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).cattax == cattaxRandomNumber } - def "PBS shouldn't remove cattax when we support ortb 2.6"() { + def "PBS shouldn't remove cattax when bidder supports ortb 2.6"() { given: "Default bid request with cattax" def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { @@ -514,28 +485,25 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - cattax == cattaxRandomNumber - } + then: "Bidder request should contain the cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).cattax == cattaxRandomNumber } - def "PBS should remove site.cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.cattax when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.cattax" + def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - site.catTax = PBSUtils.randomNumber + site.catTax = cattaxRandomNumber } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.catTax - } + then: "Bidder request should contain the site.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.catTax == cattaxRandomNumber } - def "PBS shouldn't remove site.cattax when we support ortb 2.6"() { + def "PBS shouldn't remove site.cattax when bidder supports ortb 2.6"() { given: "Default bid request with site.cattax" def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { @@ -545,30 +513,27 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.catTax == cattaxRandomNumber - } + then: "Bidder request should contain the site.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.catTax == cattaxRandomNumber } - def "PBS should remove site.content.cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.content.cattax when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.content.cattax" + def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { site.content = Content.defaultContent.tap { - cattax = PBSUtils.randomNumber + cattax = cattaxRandomNumber } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.content.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.content.cattax - } + then: "Bidder request should contain the site.content.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.cattax == cattaxRandomNumber } - def "PBS shouldn't remove site.content.cattax when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.content.cattax when bidder supports ortb 2.5"() { given: "Default bid request with site.content.cattax" def cattaxRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { @@ -580,13 +545,11 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.content.cattax as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.content.cattax == cattaxRandomNumber - } + then: "Bidder request should contain the site.content.cattax as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.cattax == cattaxRandomNumber } - def "PBS should remove imp[0].video.* when we don't support ortb 2.6"() { + def "PBS shouldn't remove imp[0].video.* and keep imp[0].video.plcmt when bidder doesn't support ortb 2.6"() { given: "Default bid request with imp[0].video.*" def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].video = Video.defaultVideo.tap { @@ -598,65 +561,43 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber] } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the imp[0].video.* as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !imp[0].video.rqddurs - !imp[0].video.maxseq - !imp[0].video.poddur - !imp[0].video.podid - !imp[0].video.podseq - !imp[0].video.mincpmpersec - !imp[0].video.slotinpod - !imp[0].video.plcmt - } + then: "Bidder request should contain the imp[0].video.* as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].video == bidRequest.imp[0].video } - def "PBS shouldn't remove imp[0].video.* when we support ortb 2.6"() { + def "PBS shouldn't remove imp[0].video.* when bidder supports ortb 2.6"() { given: "Default bid request with imp[0].video.*" - def rqddursListOfRandomNumber = [PBSUtils.randomNumber] - def maxseqRandomNumber = PBSUtils.randomNumber - def poddurRandomNumber = PBSUtils.randomNumber - def podidRandomNumber = PBSUtils.randomNumber - def podseqRandomNumber = PBSUtils.randomNumber - def mincpmpersecRandomNumber = PBSUtils.randomDecimal - def slotinpodRandomNumber = PBSUtils.randomNumber - def plcmtRandomEnum = PBSUtils.getRandomEnum(VideoPlcmtSubtype) def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].video = Video.defaultVideo.tap { - rqddurs = rqddursListOfRandomNumber - maxseq = maxseqRandomNumber - poddur = poddurRandomNumber - podid = podidRandomNumber - podseq = podseqRandomNumber - mincpmpersec = mincpmpersecRandomNumber - slotinpod = slotinpodRandomNumber - plcmt = plcmtRandomEnum + rqddurs = [PBSUtils.randomNumber] + maxseq = PBSUtils.randomNumber + poddur = PBSUtils.randomNumber + podid = PBSUtils.randomNumber + podseq = PBSUtils.randomNumber + mincpmpersec = PBSUtils.randomDecimal + slotinpod = PBSUtils.randomNumber + plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber, PBSUtils.randomNumber] } } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the imp[0].video.* as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].video.rqddurs == rqddursListOfRandomNumber - imp[0].video.maxseq == maxseqRandomNumber - imp[0].video.poddur == poddurRandomNumber - imp[0].video.podid == podidRandomNumber - imp[0].video.podseq == podseqRandomNumber - imp[0].video.mincpmpersec == mincpmpersecRandomNumber - imp[0].video.slotinpod == slotinpodRandomNumber - imp[0].video.plcmt == plcmtRandomEnum - } + then: "Bidder request should contain the imp[0].video.* as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].video == bidRequest.imp[0].video } - def "PBS should remove imp[0].audio.* when we don't support ortb 2.6"() { + def "PBS shouldn't remove imp[0].audio.* when bidder doesn't support ortb 2.6"() { given: "Default bid request with imp[0].audio.*" def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].audio = Audio.defaultAudio.tap { @@ -673,70 +614,49 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the imp[0].audio.* as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !imp[0].audio.rqddurs - !imp[0].audio.maxseq - !imp[0].audio.poddur - !imp[0].audio.podid - !imp[0].audio.podseq - !imp[0].audio.mincpmpersec - !imp[0].audio.slotinpod - } + then: "Bidder request should contain the imp[0].audio.* as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].audio == bidRequest.imp[0].audio } - def "PBS shouldn't remove imp[0].audio.* when we support ortb 2.6"() { + def "PBS shouldn't remove imp[0].audio.* when bidder supports ortb 2.6"() { given: "Default bid request with imp[0].audio.*" - def rqddursListOfRandomNumber = [PBSUtils.randomNumber] - def maxseqRandomNumber = PBSUtils.randomNumber - def poddurRandomNumber = PBSUtils.randomNumber - def podidRandomNumber = PBSUtils.randomNumber - def podseqRandomNumber = PBSUtils.randomNumber - def mincpmpersecRandomNumber = PBSUtils.randomDecimal - def slotinpodRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].audio = Audio.defaultAudio.tap { - rqddurs = rqddursListOfRandomNumber - maxseq = maxseqRandomNumber - poddur = poddurRandomNumber - podid = podidRandomNumber - podseq = podseqRandomNumber - mincpmpersec = mincpmpersecRandomNumber - slotinpod = slotinpodRandomNumber + rqddurs = [PBSUtils.randomNumber] + maxseq = PBSUtils.randomNumber + poddur = PBSUtils.randomNumber + podid = PBSUtils.randomNumber + podseq = PBSUtils.randomNumber + mincpmpersec = BigDecimal.valueOf(1) + slotinpod = PBSUtils.randomNumber } } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the imp[0].audio.* as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].audio.rqddurs == rqddursListOfRandomNumber - imp[0].audio.maxseq == maxseqRandomNumber - imp[0].audio.poddur == poddurRandomNumber - imp[0].audio.podid == podidRandomNumber - imp[0].audio.podseq == podseqRandomNumber - imp[0].audio.mincpmpersec == mincpmpersecRandomNumber - imp[0].audio.slotinpod == slotinpodRandomNumber - } + then: "Bidder request should contain the imp[0].audio.* as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].audio == bidRequest.imp[0].audio } - def "PBS should remove imp[0].ssai when we don't support ortb 2.6"() { + def "PBS shouldn't remove imp[0].ssai when bidder doesn't support ortb 2.6"() { given: "Default bid request with imp[0].ssai" + def randomSsai = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ssai = PBSUtils.randomNumber + imp[0].ssai = randomSsai } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the imp[0].ssai as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !imp[0].ssai - } + then: "Bidder request should contain the imp[0].ssai as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ssai == randomSsai } - def "PBS shouldn't remove imp[0].ssai when we support ortb 2.6"() { + def "PBS shouldn't remove imp[0].ssai when bidder supports ortb 2.6"() { given: "Default bid request with imp[0].ssai" def ssaiRandomNumber = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { @@ -746,32 +666,33 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the imp[0].ssai as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].ssai == ssaiRandomNumber - } + then: "Bidder request should contain the imp[0].ssai as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ssai == ssaiRandomNumber } - def "PBS should remove site.content.{channel, network} when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.content.{channel, network} when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.content.{network, channel}" + def defaultChannel = Channel.defaultChannel + def defaultNetwork = Network.defaultNetwork def bidRequest = BidRequest.defaultBidRequest.tap { site.content = Content.defaultContent.tap { - channel = Channel.defaultChannel - network = Network.defaultNetwork + it.channel = defaultChannel + it.network = defaultNetwork } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.content.{network, channel} as on request" + then: "Bidder request should contain the site.content.{network, channel} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.content.channel - !site.content.network + site.content.channel.id == defaultChannel.id + site.content.network.id == defaultNetwork.id } } - def "PBS shouldn't remove site.content.{channel, network} when we support ortb 2.6"() { + def "PBS shouldn't remove site.content.{channel, network} when bidder supports ortb 2.6"() { given: "Default bid request with site.content.{network, channel}" def defaultChannel = Channel.defaultChannel def defaultNetwork = Network.defaultNetwork @@ -785,33 +706,35 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.content.{channel, network} as on request" + then: "Bidder request should contain the site.content.{channel, network} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { site.content.channel.id == defaultChannel.id site.content.network.id == defaultNetwork.id } } - def "PBS should remove app.content.{channel, network} when we don't support ortb 2.6"() { + def "PBS shouldn't remove app.content.{channel, network} when bidder doesn't support ortb 2.6"() { given: "Default bid request with app.content.{network, channel}" + def defaultChannel = Channel.defaultChannel + def defaultNetwork = Network.defaultNetwork def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { app.content = Content.defaultContent.tap { - channel = Channel.defaultChannel - network = Network.defaultNetwork + channel = defaultChannel + network = defaultNetwork } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the app.content.{network, channel} as on request" + then: "Bidder request should contain the app.content.{network, channel} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !app.content.channel - !app.content.network + app.content.channel.id == defaultChannel.id + app.content.network.id == defaultNetwork.id } } - def "PBS shouldn't remove app.content.{channel, network} when we support ortb 2.6"() { + def "PBS shouldn't remove app.content.{channel, network} when bidder supports ortb 2.6"() { given: "Default bid request with content.{network, channel}" def defaultChannel = Channel.defaultChannel def defaultNetwork = Network.defaultNetwork @@ -825,29 +748,28 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the app.content.{channel, network} as on request" + then: "Bidder response should contain the app.content.{channel, network} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { app.content.channel.id == defaultChannel.id app.content.network.id == defaultNetwork.id } } - def "PBS should remove site.kwarray when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.kwarray when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.kwarray" + def randomKwArray = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { - site.kwArray = [PBSUtils.randomString] + site.kwArray = randomKwArray } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.kwArray - } + then: "Bidder request should contain the site.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).site.kwArray == randomKwArray } - def "PBS shouldn't remove site.kwarray when we support ortb 2.6"() { + def "PBS shouldn't remove site.kwarray when bidder supports ortb 2.6"() { given: "Default bid request with site.kwarray" def kwarrayRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -857,30 +779,27 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.kwArray == kwarrayRandomStrings - } + then: "Bidder request should contain the site.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).site.kwArray == kwarrayRandomStrings } - def "PBS should remove site.content.kwarray when we don't support ortb 2.6"() { + def "PBS shouldn't remove site.content.kwarray when bidder doesn't support ortb 2.6"() { given: "Default bid request with site.content.kwarray" + def kwarrayRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { site.content = Content.defaultContent.tap { - kwarray = [PBSUtils.randomString] + kwarray = kwarrayRandomStrings } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the site.content.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.content.kwarray - } + then: "Bidder request should contain the site.content.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.kwarray == kwarrayRandomStrings } - def "PBS shouldn't remove site.content.kwarray when we support ortb 2.6"() { + def "PBS shouldn't remove site.content.kwarray when bidder supports ortb 2.6"() { given: "Default bid request with site.content.kwarray" def kwarrayRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -892,28 +811,25 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the site.content.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.content.kwarray == kwarrayRandomStrings - } + then: "Bidder request should contain the site.content.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).site.content.kwarray == kwarrayRandomStrings } - def "PBS should remove app.kwarray when we don't support ortb 2.6"() { + def "PBS shouldn't remove app.kwarray when bidder doesn't support ortb 2.6"() { given: "Default bid request with app.kwarray" + def randomKwArray = [PBSUtils.randomString] def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - app.kwArray = [PBSUtils.randomString] + app.kwArray = randomKwArray } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the app.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !app.kwArray - } + then: "Bidder request should contain the app.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).app.kwArray == randomKwArray } - def "PBS shouldn't remove app.kwarray when we support ortb 2.6"() { + def "PBS shouldn't remove app.kwarray when bidder supports ortb 2.6"() { given: "Default bid request with app.kwarray" def kwarrayRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { @@ -923,30 +839,27 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the app.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - app.kwArray == kwarrayRandomStrings - } + then: "Bidder request should contain the app.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).app.kwArray == kwarrayRandomStrings } - def "PBS should remove user.kwarray when we don't support ortb 2.6"() { + def "PBS shouldn't remove user.kwarray when bidder doesn't support ortb 2.6"() { given: "Default bid request with user.kwarray" + def kwarrayRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { user = User.defaultUser.tap { - kwarray = [PBSUtils.randomString] + kwarray = kwarrayRandomStrings } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the user.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !user.kwarray - } + then: "Bidder request shouldn't contain the user.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).user.kwarray == kwarrayRandomStrings } - def "PBS shouldn't remove user.kwarray when we support ortb 2.6"() { + def "PBS shouldn't remove user.kwarray when bidder supports ortb 2.6"() { given: "Default bid request with user.kwarray" def kwarrayRandomStrings = [PBSUtils.randomString] def bidRequest = BidRequest.defaultBidRequest.tap { @@ -958,18 +871,17 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the user.kwarray as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - user.kwarray == kwarrayRandomStrings - } + then: "Bidder request should contain the user.kwarray as on request" + assert bidder.getBidderRequest(bidRequest.id).user.kwarray == kwarrayRandomStrings } - def "PBS should remove device.sua when we don't support ortb 2.6"() { + def "PBS shouldn't remove device.sua when bidder doesn't support ortb 2.6"() { given: "Default bid request with device.sua" + def model = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { device = new Device().tap { sua = new UserAgent().tap { - model = PBSUtils.randomString + it.model = model } } } @@ -977,19 +889,17 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the device.sua as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !device.sua - } + then: "Bidder request should contain the device.sua as on request" + assert bidder.getBidderRequest(bidRequest.id).device.sua.model == model } - def "PBS shouldn't remove device.sua when we support ortb 2.6"() { + def "PBS shouldn't remove device.sua when bidder supports ortb 2.6"() { given: "Default bid request with device.sua" - def modelRandomString = PBSUtils.randomString + def model = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { device = new Device().tap { sua = new UserAgent().tap { - model = modelRandomString + it.model = model } } } @@ -997,10 +907,8 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the device.sua as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - device.sua.model == modelRandomString - } + then: "Bidder request should contain the device.sua as on request" + assert bidder.getBidderRequest(bidRequest.id).device.sua.model == model } def "PBS should pass bid[].{langb, dur, slotinpor, apis, cattax} through to response"() { @@ -1013,12 +921,14 @@ class OrtbConverterSpec extends BaseSpec { def apisRandomNumbers = [PBSUtils.randomNumber] def slotinpodRandomNumber = PBSUtils.randomNumber def cattaxRandomNumber = PBSUtils.randomNumber + def catRandomNumber = [PBSUtils.randomString] def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { seatbid.first().bid.first().langb = langbRandomString seatbid.first().bid.first().dur = durRandomNumber seatbid.first().bid.first().apis = apisRandomNumbers seatbid.first().bid.first().slotinpod = slotinpodRandomNumber seatbid.first().bid.first().cattax = cattaxRandomNumber + seatbid.first().bid.first().cat = catRandomNumber } and: "Set bidder response" @@ -1027,30 +937,32 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" def response = prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the lang, dur, apis, slotinpod, cattax as on request" + then: "Bidder request should contain the lang, dur, apis, slotinpod, cattax,cat as on request" verifyAll(response) { seatbid.first().bid.first().langb == langbRandomString seatbid.first().bid.first().dur == durRandomNumber seatbid.first().bid.first().apis == apisRandomNumbers seatbid.first().bid.first().slotinpod == slotinpodRandomNumber seatbid.first().bid.first().cattax == cattaxRandomNumber + seatbid.first().bid.first().cat == catRandomNumber } } - def "PBS should remove gpp and gppSid when PBS don't support ortb 2.6"() { + def "PBS shouldn't remove gpp and gppSid when PBS don't support ortb 2.6"() { given: "Default bid request with device.sua" + def randomGpp = PBSUtils.randomString + def randomGppSid = [PBSUtils.getRandomNumber(), PBSUtils.getRandomNumber()] def bidRequest = BidRequest.defaultBidRequest.tap { - regs = new Regs(gpp: PBSUtils.randomString, gppSid: [PBSUtils.getRandomNumber(), - PBSUtils.getRandomNumber()]) + regs = new Regs(gpp: randomGpp, gppSid: randomGppSid) } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidderRequest shouldn't contain the regs.gpp and regs.gppSid as on request" + then: "BidderRequest should contain the regs.gpp and regs.gppSid as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !regs.gpp - !regs.gppSid + regs.gpp == bidRequest.regs.gpp + regs.gppSid.eachWithIndex { value, i -> bidRequest.regs.gppSid[i] == value } } } @@ -1067,122 +979,114 @@ class OrtbConverterSpec extends BaseSpec { then: "BidderRequest should contain the regs.gpp and regs.gppSid as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { regs.gpp == bidRequest.regs.gpp - regs.gppSid.eachWithIndex { Integer value, int i -> bidRequest.regs.gppSid[i] == value } + regs.gppSid.eachWithIndex { value, i -> bidRequest.regs.gppSid[i] == value } } } - def "PBS should remove imp[0].{refresh/qty/dt} when we don't support ortb 2.6"() { + def "PBS shouldn't remove imp[0].{refresh/qty/dt} when bidder doesn't support ortb 2.6"() { given: "Default bid request with imp[0].{refresh/qty/dt}" + def refresh = new Refresh(count: PBSUtils.randomNumber, refSettings: + [new RefSettings(refType: PBSUtils.getRandomEnum(RefType), minInt: PBSUtils.randomNumber)]) + def qty = new Qty(multiplier: PBSUtils.randomDecimal, sourceType: PBSUtils.getRandomEnum(SourceType), + vendor: PBSUtils.randomString) + def dt = PBSUtils.randomDecimal def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].tap { - refresh = new Refresh(count: PBSUtils.randomNumber, refSettings: [new RefSettings( - refType: PBSUtils.getRandomEnum(RefType), - minInt: PBSUtils.randomNumber)]) - qty = new Qty(multiplier: PBSUtils.randomDecimal, - sourceType: PBSUtils.getRandomEnum(SourceType), - vendor: PBSUtils.randomString) - dt = PBSUtils.randomDecimal + it.refresh = refresh + it.qty = qty + it.dt = dt } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the imp[0].{refresh/qty/dt} as on request" + then: "Bidder request should contain the imp[0].{refresh/qty/dt} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !imp[0].refresh - !imp[0].qty - !imp[0].dt + imp[0].refresh == refresh + imp[0].qty == qty + imp[0].dt == dt } } - def "PBS shouldn't remove imp[0].{refresh/qty/dt} when we support ortb 2.6"() { + def "PBS shouldn't remove imp[0].{refresh/qty/dt} when bidder supports ortb 2.6"() { given: "Default bid request with imp[0].{refresh/qty/dt}" def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].tap { - refresh = new Refresh(count: PBSUtils.randomNumber, refSettings: [new RefSettings( - refType: PBSUtils.getRandomEnum(RefType), - minInt: PBSUtils.randomNumber)]) - qty = new Qty(multiplier: PBSUtils.randomDecimal, + it.refresh = new Refresh(count: PBSUtils.randomNumber, refSettings: + [new RefSettings(refType: PBSUtils.getRandomEnum(RefType), minInt: PBSUtils.randomNumber)]) + it.qty = new Qty(multiplier: PBSUtils.randomDecimal, sourceType: PBSUtils.getRandomEnum(SourceType), vendor: PBSUtils.randomString) - dt = PBSUtils.randomDecimal + it.dt = PBSUtils.randomDecimal } } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the imp[0].{refresh/qty/dt} as on request" + then: "Bidder request should contain the imp[0].{refresh/qty/dt} as on request" verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].refresh.count == bidRequest.imp[0].refresh.count - imp[0].refresh.refSettings[0].refType == bidRequest.imp[0].refresh.refSettings[0].refType - imp[0].refresh.refSettings[0].minInt == bidRequest.imp[0].refresh.refSettings[0].minInt - imp[0].qty.multiplier == bidRequest.imp[0].qty.multiplier - imp[0].qty.sourceType == bidRequest.imp[0].qty.sourceType - imp[0].qty.vendor == bidRequest.imp[0].qty.vendor + imp[0].refresh == bidRequest.imp[0].refresh + imp[0].qty == bidRequest.imp[0].qty imp[0].dt == bidRequest.imp[0].dt } } - def "PBS should remove site.inventoryPartnerDomain when PBS don't support ortb 2.6"() { + def "PBS shouldn't remove site.inventoryPartnerDomain when PBS don't support ortb 2.6"() { given: "Default bid request with site.inventoryPartnerDomain" + def inventoryPartnerDomain = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { - site.inventoryPartnerDomain = PBSUtils.randomString + site.inventoryPartnerDomain = inventoryPartnerDomain } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidderRequest shouldn't contain the app.inventoryPartnerDomain as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !site.inventoryPartnerDomain - } + then: "BidderRequest should contain the app.inventoryPartnerDomain as on request" + assert bidder.getBidderRequest(bidRequest.id).site.inventoryPartnerDomain == inventoryPartnerDomain } def "PBS shouldn't remove site.inventoryPartnerDomain when PBS support ortb 2.6"() { given: "Default bid request with site.inventoryPartnerDomain" + def inventoryPartnerDomain = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { - site.inventoryPartnerDomain = PBSUtils.randomString + site.inventoryPartnerDomain = inventoryPartnerDomain } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) then: "BidderRequest should contain the site.inventoryPartnerDomain as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - site.inventoryPartnerDomain == bidRequest.site.inventoryPartnerDomain - } + assert bidder.getBidderRequest(bidRequest.id).site.inventoryPartnerDomain == inventoryPartnerDomain } - def "PBS should remove app.inventoryPartnerDomain when PBS don't support ortb 2.6"() { + def "PBS shouldn't remove app.inventoryPartnerDomain when PBS don't support ortb 2.6"() { given: "Default bid request with app.inventoryPartnerDomain" + def inventoryPartnerDomain = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - app.inventoryPartnerDomain = PBSUtils.randomString + app.inventoryPartnerDomain = inventoryPartnerDomain } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidderRequest shouldn't contain the app.inventoryPartnerDomain as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !app.inventoryPartnerDomain - } + then: "Bidder request should contain the app.inventoryPartnerDomain as on request" + assert bidder.getBidderRequest(bidRequest.id).app.inventoryPartnerDomain == inventoryPartnerDomain } def "PBS shouldn't remove app.inventoryPartnerDomain when PBS support ortb 2.6"() { given: "Default bid request with app.inventoryPartnerDomain" + def inventoryPartnerDomain = PBSUtils.randomString def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - app.inventoryPartnerDomain = PBSUtils.randomString + app.inventoryPartnerDomain = inventoryPartnerDomain } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidderRequest should contain the app.inventoryPartnerDomain as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - app.inventoryPartnerDomain == bidRequest.app.inventoryPartnerDomain - } + then: "Bidder request should contain the app.inventoryPartnerDomain as on request" + assert bidder.getBidderRequest(bidRequest.id).app.inventoryPartnerDomain == inventoryPartnerDomain } def "PBS should remove bidRequest.dooh when PBS don't support ortb 2.6"() { @@ -1204,10 +1108,8 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidderRequest shouldn't contain the bidRequest.dooh as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !dooh - } + then: "Bidder request should contain the bidRequest.dooh as on request" + assert bidder.getBidderRequest(bidRequest.id).dooh == bidRequest.dooh } def "PBS shouldn't remove bidRequest.dooh when PBS support ortb 2.6"() { @@ -1229,18 +1131,8 @@ class OrtbConverterSpec extends BaseSpec { when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidderRequest should contain the bidRequest.dooh as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - dooh.id == bidRequest.dooh.id - dooh.name == bidRequest.dooh.name - dooh.venueType == bidRequest.dooh.venueType - dooh.venueTypeTax == bidRequest.dooh.venueTypeTax - dooh.publisher.id == bidRequest.dooh.publisher.id - dooh.domain == bidRequest.dooh.domain - dooh.keywords == bidRequest.dooh.keywords - dooh.content.id == bidRequest.dooh.content.id - dooh.ext.data == bidRequest.dooh.ext.data - } + then: "Bidder request should contain the bidRequest.dooh as on request" + assert bidder.getBidderRequest(bidRequest.id).dooh == bidRequest.dooh } def "PBS shouldn't remove regs.ext.gpc when ortb request support ortb 2.6"() { @@ -1248,17 +1140,15 @@ class OrtbConverterSpec extends BaseSpec { def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { regs = Regs.defaultRegs.tap { - ext.gpc = randomGpc + ext = new RegsExt(gpc: randomGpc) } } when: "Requesting PBS auction with ortb 2.6" prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same regs as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - regs.ext.gpc == randomGpc - } + then: "Bidder request should contain the same regs as on request" + assert bidder.getBidderRequest(bidRequest.id).regs.ext.gpc == randomGpc } def "PBS shouldn't remove regs.ext.gpc when ortb request doesn't support ortb 2.6"() { @@ -1266,16 +1156,138 @@ class OrtbConverterSpec extends BaseSpec { def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { regs = Regs.defaultRegs.tap { - ext.gpc = randomGpc + ext = new RegsExt(gpc: randomGpc) } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse should contain the same regs as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - regs.ext.gpc == randomGpc + then: "Bidder request should contain the same regs as on request" + assert bidder.getBidderRequest(bidRequest.id).regs.ext.gpc == randomGpc + } + + def "PBS shouldn't remove video.protocols when ortb request support 2.6"() { + given: "Default bid request with Banner object" + def protocols = [PBSUtils.randomNumber] + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].video = Video.getDefaultVideo().tap { + it.protocols = protocols + } + } + + when: "Requesting PBS auction with ortb 2.6" + prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain video.protocols on request" + assert bidder.getBidderRequest(bidRequest.id).imp[0].video.protocols == protocols + } + + def "PBS shouldn't remove video.protocols when ortb request support 2.5"() { + given: "Default bid request with Banner object" + def protocols = [PBSUtils.randomNumber] + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].video = Video.getDefaultVideo().tap { + it.protocols = protocols + } + } + + when: "Requesting PBS auction with ortb 2.5" + prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain video.protocols on request" + assert bidder.getBidderRequest(bidRequest.id).imp[0].video.protocols == protocols + } + + def "PBS shouldn't remove saetbid[0].bid[].{lang,dur.slotinpod,apis,cat,cattax} when ortb request support 2.5"() { + given: "Default bid request with stored request object" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(bidRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response" + def langb = PBSUtils.randomString + def dur = PBSUtils.randomNumber + def slotinpod = PBSUtils.randomNumber + def apis = [PBSUtils.randomNumber] + def cat = [PBSUtils.randomString] + def cattax = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].tap { + it.langb = langb + it.dur = dur + it.slotinpod = slotinpod + it.apis = apis + it.cat = cat + it.cattax = cattax + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Requesting PBS auction with ortb 2.5" + def response = prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain seat[0].bid[0].{langb,dur,slotinpod,apis,cattax,cat} on request" + verifyAll(response.seatbid[0].bid[0]) { + it.langb == langb + it.dur == dur + it.slotinpod == slotinpod + it.apis == apis + it.cattax == cattax + it.cat == cat + } + } + + def "PBS shouldn't remove saetbid[0].bid[].{lang,dur.slotinpod,apis,cat,cattax} when ortb request support 2.6"() { + given: "Default bid request with stored request object" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(bidRequest) + storedRequestDao.save(storedRequest) + + and: "Default bidder response " + def langb = PBSUtils.randomString + def dur = PBSUtils.randomNumber + def slotinpod = PBSUtils.randomNumber + def apis = [PBSUtils.randomNumber] + def cat = [PBSUtils.randomString] + def cattax = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].tap { + it.langb = langb + it.dur = dur + it.slotinpod = slotinpod + it.apis = apis + it.cattax = cattax + it.cat = cat + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Requesting PBS auction with ortb 2.6" + def response = prebidServerServiceWithNewOrtb.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain seat[0].bid[0].{langb,dur,slotinpod,apis,cattax,cat} on request" + verifyAll(response.seatbid[0].bid[0]) { + it.langb == langb + it.dur == dur + it.slotinpod == slotinpod + it.apis == apis + it.cattax == cattax + it.cat == cat } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy index d409546c160..7acfb56d2ca 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy @@ -1,25 +1,44 @@ package org.prebid.server.functional.tests import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountBidValidationConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.util.PBSUtils +import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.NO_BID -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.OTHER_ERROR -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_MEDIA_TYPE -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.TIMED_OUT +import static org.mockserver.model.HttpStatusCode.PROCESSING_102 +import static org.mockserver.model.HttpStatusCode.SERVICE_UNAVAILABLE_503 +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_BIDDER_UNREACHABLE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_INVALID_BID_RESPONSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_NO_BID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_TIMED_OUT +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC class SeatNonBidSpec extends BaseSpec { def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder didn't bid"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true - } + def bidRequest = requestWithAllBidStatus and: "Default bidder response without bid" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -38,24 +57,21 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == NO_BID + assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID where: responseStatusCode << [OK_200, NO_CONTENT_204] } - def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with non-SUCCESS status code"() { + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with invalid bid response status code"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true - } + def bidRequest = requestWithAllBidStatus - and: "Default bidder response without bid" + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) and: "Set bidder response" - def successStatuses = [OK_200, NO_CONTENT_204] - def statusCode = PBSUtils.getRandomElement(HttpStatusCode.values() - successStatuses as List) + def statusCode = PBSUtils.getRandomElement([PROCESSING_102, BAD_REQUEST_400, INTERNAL_SERVER_ERROR_500]) bidder.setResponse(bidRequest.id, bidResponse, statusCode) when: "PBS processes auction request" @@ -67,16 +83,100 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == OTHER_ERROR + assert seatNonBid.nonBid[0].statusCode == ERROR_INVALID_BID_RESPONSE } - def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() { + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with bidder unreachable status code"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse, SERVICE_UNAVAILABLE_503) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == ERROR_BIDDER_UNREACHABLE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with invalid creative size status code"() { + given: "Default bid request with returnAllBidStatus" + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response with creative size adjustment" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.tap { + bid.first.height = bidRequest.imp.first.banner.format.first.height + 1 + bid.first.weight = bidRequest.imp.first.banner.format.first.weight + 1 + } } - and: "Default bidder response without bid" + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(bidValidations: + new AccountBidValidationConfig(bannerMaxSizeEnforcement: ENFORCE))) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_SIZE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with not secure status code"() { + given: "PBS with secure-markup enforcement" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": ENFORCE.value]) + + and: "A bid request with secure and returnAllBidStatus flags set" + def bidRequest = requestWithAllBidStatus.tap { + imp[0].secure = SECURE + } + + and: "A default bidder response without a valid bid" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.bid.first.tap { + it.adm = new Adm(assets: [Asset.getImgAsset("http://secure-assets.${PBSUtils.randomString}.com")]) + } + } + + and: "Setting the bidder response" + bidder.setResponse(bidRequest.id, storedBidResponse) + + when: "PBS processes the auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "The PBS response should contain seatNonBid for the called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE + } + + def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() { + given: "Default bid request with returnAllBidStatus" + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) and: "Set bidder response" @@ -85,19 +185,18 @@ class SeatNonBidSpec extends BaseSpec { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) - then: "PBS response should contain seatNonBid" + then: "PBS response shouldn't contain seatNonBid" assert !response.ext.seatnonbid assert response.seatbid } def "PBS should populate seatNonBid when returnAllBidStatus=true and debug=#debug and requested bidder didn't bid for any reason"() { given: "Default bid request with returnAllBidStatus and debug = #debug" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus.tap { ext.prebid.debug = debug } - and: "Default bidder response without bid" + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { seatbid = [] } @@ -114,7 +213,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == NO_BID + assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID and: "PBS response shouldn't contain seatBid" assert !response.seatbid @@ -155,8 +254,7 @@ class SeatNonBidSpec extends BaseSpec { def pbsService = pbsServiceFactory.getService(["auction.biddertmax.min": timeout as String]) and: "Default bid request with max timeout" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus.tap { tmax = timeout } @@ -176,7 +274,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == TIMED_OUT + assert seatNonBid.nonBid[0].statusCode == ERROR_TIMED_OUT } def "PBS should populate seatNonBid when filter-imp-media-type=true and imp doesn't contain supported media type"() { @@ -200,9 +298,37 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_BY_MEDIA_TYPE + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE and: "seatbid should be empty" assert response.seatbid.isEmpty() } + + def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and storedAuctionResponse present"() { + given: "Default bid request with returnAllBidStatus and storedAuction" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + } + + and: "Stored auction response in DB" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + assert response.seatbid + } + + private static BidRequest getRequestWithAllBidStatus(DistributionChannel channel = SITE) { + BidRequest.getDefaultBidRequest(channel).tap { + ext.prebid.returnAllBidStatus = true + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy index d12334da01b..c7cceae6d01 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy @@ -13,7 +13,6 @@ import org.prebid.server.functional.util.PBSUtils import org.prebid.server.util.ResourceUtil import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.bidder.BidderName.bidderNameByString import static org.prebid.server.functional.model.response.status.Status.OK class SmokeSpec extends BaseSpec { diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy index 142a0b7347c..fccb14c8bab 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy @@ -2,10 +2,14 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature @@ -120,4 +124,255 @@ class StoredResponseSpec extends BaseSpec { and: "PBS not send request to bidder" assert bidder.getRequestCount(bidRequest.id) == 0 } + + def "PBS should return warning when imp[0].ext.prebid.storedAuctionResponse contain seatBid"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + id = storedResponseId + seatBids = [storedAuctionResponse] + } + + and: "Stored auction response in DB" + def storedResponse = new StoredResponse(responseId: storedResponseId, storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain warning information" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == + ['WARNING: request.imp[0].ext.prebid.storedauctionresponse.seatbidarr is not supported at the imp level'] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should set seatBid from request storedAuctionResponse.seatBid when ext.prebid.storedAuctionResponse.seatBid present and id is null"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + id = null + seatBids = [storedAuctionResponse] + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert response.seatbid == [storedAuctionResponse] + + and: "PBs should emit warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == + ["no auction. response defined by storedauctionresponse" as String] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should set seatBid in response from db when ext.prebid.storedAuctionResponse.seatBid not defined and id is defined"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + id = storedResponseId + seatBids = null + } + + and: "Stored auction response in DB" + def storedResponse = new StoredResponse(responseId: storedResponseId, storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert response.seatbid == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should perform usually auction call when storedActionResponse when id and seatbid are null"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + it.id = null + it.seatBids = null + } + + and: "Stored auction response in DB" + def storedResponse = new StoredResponse(responseId: storedResponseId, storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert response.seatbid + + and: "PBs shouldn't emit warnings" + assert !response.ext?.warnings + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 1 + + where: + seatbid << [null, [null]] + } + + def "PBS return warning when id is null and seatbid with null"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + it.id = null + it.seatBids = [null] + } + + and: "Stored auction response in DB" + def storedResponse = new StoredResponse(responseId: storedResponseId, storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain warning information" + assert response.ext?.warnings[ErrorType.PREBID]*.message.contains('SeatBid can\'t be null in stored response') + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should set seatBid in response from single imp.ext.prebid.storedBidResponse.seatbidobj when it is defined"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: storedAuctionResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty seatbid"() { + given: "Default basic BidRequest with empty stored response" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid()) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: Seat can\'t be empty in stored response seatBid' + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty bids"() { + given: "Default basic BidRequest with empty bids for stored response" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid(bid: [], seat: GENERIC)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: There must be at least one bid in stored response seatBid' + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should prefer seatbidobj over storedAuctionResponse.id from imp when both are present"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + id = PBSUtils.randomString + seatBidObject = storedAuctionResponse + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should set seatBids in response from multiple imp.ext.prebid.storedBidResponse.seatbidobj when it is defined"() { + given: "BidRequest with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [impWithSeatBidObject, impWithSeatBidObject] + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response bids as requested" + assert convertToComparableSeatBid(response.seatbid).bid.flatten().sort() == + bidRequest.imp.ext.prebid.storedAuctionResponse.seatBidObject.bid.flatten().sort() + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should prefer seatbidarr from request over seatbidobj from imp when both are present"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.tap{ + imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + seatBidObject = SeatBid.getStoredResponse(bidRequest) + } + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBids: [storedAuctionResponse]) + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert response.seatbid == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + private static final Imp getImpWithSeatBidObject() { + def imp = Imp.defaultImpression + def bids = Bid.getDefaultBids([imp]) + def seatBid = new SeatBid(bid: bids, seat: GENERIC) + imp.tap { + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: seatBid) + } + } + + private static final List convertToComparableSeatBid(List seatBid) { + seatBid*.tap { + it.bid*.ext = null + it.group = null + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy index d58e6e3a781..e24d22b4b8f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.PriceGranularityType import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse @@ -17,23 +18,29 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode - import java.nio.charset.StandardCharsets + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.PriceGranularityType.UNKNOWN import static org.prebid.server.functional.model.response.auction.ErrorType.TARGETING import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class TargetingSpec extends BaseSpec { private static final Integer TARGETING_PARAM_NAME_MAX_LENGTH = 20 + private static final Integer TARGETING_KEYS_SIZE = 14 private static final Integer MAX_AMP_TARGETING_TRUNCATION_LENGTH = 11 private static final String DEFAULT_TARGETING_PREFIX = "hb" private static final Integer TARGETING_PREFIX_LENGTH = 11 private static final Integer MAX_TRUNCATE_ATTR_CHARS = 255 + private static final String HB_ENV_AMP = "amp" def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -390,7 +397,7 @@ class TargetingSpec extends BaseSpec { } and: "Account in the DB" - def targetingLength = PBSUtils.getRandomNumber(2,10) + def targetingLength = PBSUtils.getRandomNumber(2, 10) def account = new Account(uuid: accountId, truncateTargetAttr: targetingLength) accountDao.save(account) @@ -435,7 +442,7 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) and: "Create and save account in the DB" - def account = new Account(uuid: ampRequest.account, truncateTargetAttr: PBSUtils.getRandomNumber(1,10)) + def account = new Account(uuid: ampRequest.account, truncateTargetAttr: PBSUtils.getRandomNumber(1, 10)) accountDao.save(account) when: "PBS processes amp request" @@ -458,7 +465,8 @@ class TargetingSpec extends BaseSpec { ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { priceGranularity = new PriceGranularity().tap { it.precision = precision - ranges = [new Range(max: max, increment: PBSUtils.randomDecimal)]} + ranges = [new Range(max: max, increment: PBSUtils.randomDecimal)] + } } } @@ -527,7 +535,7 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain default targeting prefix" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert targeting.size() == 6 - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } def "PBS auction should use default targeting prefix when auction.config.targeting.prefix is biggest that twenty"() { @@ -539,7 +547,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) - def account = new Account(uuid: bidRequest.accountId,config: new AccountConfig(auction: config) ) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes auction request" @@ -548,7 +556,7 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain default targeting prefix" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert targeting.size() == 6 - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } def "PBS auction should default targeting prefix when ext.prebid.targeting.prefix is #prefix"() { @@ -563,9 +571,10 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain default targeting prefix" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert targeting.size() == 6 - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } - where: prefix << [null, ""] + where: + prefix << [null, ""] } def "PBS auction should default targeting prefix when auction.targeting.prefix is #prefix"() { @@ -576,7 +585,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) - def account = new Account(uuid: bidRequest.accountId,config: new AccountConfig(auction: config) ) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes auction request" @@ -585,9 +594,10 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain default targeting prefix" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert targeting.size() == 6 - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } - where: prefix << [null, ""] + where: + prefix << [null, ""] } def "PBS auction should update targeting prefix when ext.prebid.targeting.prefix specified"() { @@ -603,7 +613,7 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain targeting with requested prefix" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert !targeting.isEmpty() - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS auction should update prefix name for targeting when account specified"() { @@ -615,7 +625,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) - def account = new Account(uuid: bidRequest.accountId,config: new AccountConfig(auction: config) ) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes auction request" @@ -623,7 +633,7 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain targeting key with specified prefix in account level" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS auction should update targeting prefix and take precedence request level over account when prefix specified in both place"() { @@ -635,7 +645,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: "account_")) - def account = new Account(uuid: bidRequest.accountId,config: new AccountConfig(auction: config) ) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes auction request" @@ -643,7 +653,7 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain targeting key with specified prefix in account level" def targeting = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS amp should trim targeting prefix when ext.prebid.targeting.prefix targeting is biggest that twenty"() { @@ -665,8 +675,8 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain default targeting prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.size() == TARGETING_KEYS_SIZE + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } def "PBS amp should trim targeting prefix when auction.config.targeting.prefix targeting is biggest that twenty"() { @@ -679,7 +689,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def prefix = PBSUtils.getRandomString(30) def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) - def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config) ) + def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config)) accountDao.save(account) and: "Create and save stored request into DB" @@ -691,8 +701,8 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.size() == TARGETING_KEYS_SIZE + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } def "PBS amp should default targeting prefix when auction.config.targeting.prefix is #prefix"() { @@ -708,7 +718,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) - def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config) ) + def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes amp request" @@ -717,9 +727,10 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } - where: prefix << [null, ""] + where: + prefix << [null, ""] } def "PBS amp should default targeting prefix when ext.prebid.targeting is #prefix"() { @@ -741,9 +752,10 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } - where: prefix << [null, ""] + where: + prefix << [null, ""] } def "PBS amp should update targeting prefix when specified in account prefix"() { @@ -760,7 +772,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) - def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config) ) + def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes amp request" @@ -769,7 +781,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS amp should use custom prefix for targeting when stored request ext.prebid.targeting.prefix specified"() { @@ -792,7 +804,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain custom targeting prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS amp should take precedence from ext.prebid.targeting.prefix when specified in account targeting prefix"() { @@ -811,7 +823,7 @@ class TargetingSpec extends BaseSpec { and: "Account in the DB" def config = new AccountAuctionConfig(targeting: new Targeting(prefix: "account_")) - def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config) ) + def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes amp request" @@ -820,7 +832,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS amp should move targeting key to imp.ext.data"() { @@ -830,7 +842,7 @@ class TargetingSpec extends BaseSpec { } and: "Encode Targeting to String" - def encodeTargeting = URLEncoder.encode(encode(targeting), StandardCharsets.UTF_8) + def encodeTargeting = URLEncoder.encode(encode(targeting), StandardCharsets.UTF_8) and: "Amp request with targeting" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -853,10 +865,10 @@ class TargetingSpec extends BaseSpec { } def "PBS amp should use long account targeting prefix when settings.targeting.truncate-attr-chars override"() { - given:"PBS config with setting.targeting" - def prefixMaxChars = PBSUtils.getRandomNumber(35,MAX_TRUNCATE_ATTR_CHARS) + given: "PBS config with setting.targeting" + def prefixMaxChars = PBSUtils.getRandomNumber(35, MAX_TRUNCATE_ATTR_CHARS) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) + ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) and: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest @@ -880,14 +892,14 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS amp should use long request targeting prefix when settings.targeting.truncate-attr-chars override"() { - given:"PBS config with setting.targeting" - def prefixMaxChars = PBSUtils.getRandomNumber(35,MAX_TRUNCATE_ATTR_CHARS) + given: "PBS config with setting.targeting" + def prefixMaxChars = PBSUtils.getRandomNumber(35, MAX_TRUNCATE_ATTR_CHARS) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) + ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) and: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest @@ -908,16 +920,16 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting assert !targeting.isEmpty() - assert targeting.keySet().every { it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS auction should use long request targeting prefix when settings.targeting.truncate-attr-chars override"() { - given:"PBS config with setting.targeting" - def prefixMaxChars = PBSUtils.getRandomNumber(35,MAX_TRUNCATE_ATTR_CHARS) + given: "PBS config with setting.targeting" + def prefixMaxChars = PBSUtils.getRandomNumber(35, MAX_TRUNCATE_ATTR_CHARS) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) + ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) - and:"Bid request with prefix" + and: "Bid request with prefix" def prefix = PBSUtils.getRandomString(prefixMaxChars - TARGETING_PREFIX_LENGTH) def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(prefix: prefix) @@ -929,14 +941,14 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain default targeting prefix" def targeting = bidResponse.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert !targeting.isEmpty() - assert targeting.keySet().every{it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS auction should use long account targeting prefix when settings.targeting.truncate-attr-chars override"() { - given:"PBS config with setting.targeting" - def prefixMaxChars = PBSUtils.getRandomNumber(35,MAX_TRUNCATE_ATTR_CHARS) + given: "PBS config with setting.targeting" + def prefixMaxChars = PBSUtils.getRandomNumber(35, MAX_TRUNCATE_ATTR_CHARS) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) + ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) and: "Bid request with empty targeting" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -955,14 +967,14 @@ class TargetingSpec extends BaseSpec { then: "PBS response should contain default targeting prefix" def targeting = bidResponse.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert !targeting.isEmpty() - assert targeting.keySet().every{it -> it.startsWith(prefix)} + assert targeting.keySet().every { it -> it.startsWith(prefix) } } def "PBS amp should ignore and add a warning to ext.warnings when value of the account prefix is longer then settings.targeting.truncate-attr-chars"() { - given:"PBS config with setting.targeting" - def targetingChars = PBSUtils.getRandomNumber(2,10) + given: "PBS config with setting.targeting" + def targetingChars = PBSUtils.getRandomNumber(2, 10) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) + ["settings.targeting.truncate-attr-chars": targetingChars as String]) and: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest @@ -985,15 +997,15 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain warning" assert ampResponse.ext?.warnings[TARGETING]*.message == ["Key prefix value is dropped to default. " + - "Decrease custom prefix length or increase truncateattrchars by " + - "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] + "Decrease custom prefix length or increase truncateattrchars by " + + "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] } def "PBS amp should ignore and add a warning to ext.warnings when value of the request prefix is longer then settings.targeting.truncate-attr-chars"() { - given:"PBS config with setting.targeting" - def targetingChars = PBSUtils.getRandomNumber(2,10) + given: "PBS config with setting.targeting" + def targetingChars = PBSUtils.getRandomNumber(2, 10) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) + ["settings.targeting.truncate-attr-chars": targetingChars as String]) and: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest @@ -1013,17 +1025,17 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain warning" assert ampResponse.ext?.warnings[TARGETING]*.message == ["Key prefix value is dropped to default. " + - "Decrease custom prefix length or increase truncateattrchars by " + - "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] + "Decrease custom prefix length or increase truncateattrchars by " + + "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] } def "PBS auction should ignore and add a warning to ext.warnings when value of the request prefix is longer then settings.targeting.truncate-attr-chars"() { - given:"PBS config with setting.targeting" - def targetingChars = PBSUtils.getRandomNumber(2,10) + given: "PBS config with setting.targeting" + def targetingChars = PBSUtils.getRandomNumber(2, 10) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) + ["settings.targeting.truncate-attr-chars": targetingChars as String]) - and:"Bid request with prefix" + and: "Bid request with prefix" def prefixSize = targetingChars + 1 def prefix = PBSUtils.getRandomString(prefixSize) def bidRequest = BidRequest.defaultBidRequest.tap { @@ -1036,17 +1048,17 @@ class TargetingSpec extends BaseSpec { then: "Bid response should contain warning" def targeting = bidResponse.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert !targeting.isEmpty() - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } assert bidResponse.ext?.warnings[TARGETING]*.message == ["Key prefix value is dropped to default. " + - "Decrease custom prefix length or increase truncateattrchars by " + - "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] + "Decrease custom prefix length or increase truncateattrchars by " + + "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] } def "PBS auction should ignore and add a warning to ext.warnings when value of the account prefix is longer then settings.targeting.truncate-attr-chars"() { given: "PBS config with setting.targeting" - def targetingChars = PBSUtils.getRandomNumber(2,10) + def targetingChars = PBSUtils.getRandomNumber(2, 10) def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) + ["settings.targeting.truncate-attr-chars": targetingChars as String]) and: "Bid request" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -1065,17 +1077,284 @@ class TargetingSpec extends BaseSpec { then: "Bid response should contain warning" def targeting = bidResponse.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert !targeting.isEmpty() - assert targeting.keySet().every{it -> it.startsWith(DEFAULT_TARGETING_PREFIX)} + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } assert bidResponse.ext?.warnings[TARGETING]*.message == ["Key prefix value is dropped to default. " + - "Decrease custom prefix length or increase truncateattrchars by " + - "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] + "Decrease custom prefix length or increase truncateattrchars by " + + "${prefix.length() + TARGETING_PREFIX_LENGTH - targetingChars}"] + } + + def "PBS amp should apply data from query to ext.prebid.amp.data"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Bid request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def unknownValue = PBSUtils.randomString + def secondUnknownValue = PBSUtils.randomNumber + defaultPbsService.sendAmpRequestWithAdditionalQueries(ampRequest, ["unknown_field" : unknownValue, + "second_unknown_field": secondUnknownValue]) + + then: "Amp should contain data from query request" + def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) + def ampData = bidderRequests.ext.prebid.amp.data + assert ampData.unknownField == unknownValue + assert ampData.secondUnknownField == secondUnknownValue + } + + def "PBS amp should always send hb_env=amp when stored request does not contain app"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default bid request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Amp response should contain amp hb_env" + def targeting = ampResponse.targeting + assert targeting["hb_env"] == HB_ENV_AMP + } + + def "PBS auction should throw error when price granularity from original request is empty"() { + given: "Default bidRequest with empty price granularity" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS auction should prioritize price granularity from original request over account config"() { + given: "Default bidRequest with price granularity" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity as PriceGranularityType) + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + } + + and: "Account in the DB" + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: PBSUtils.getRandomEnum(PriceGranularityType)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS amp should prioritize price granularity from original request over account config"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity) + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config with different name case when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from default account config when original request doesn't contain price granularity"() { + given: "Pbs with default account that include privacySandbox configuration" + def priceGranularity = PBSUtils.getRandomEnum(PriceGranularityType, [UNKNOWN]) + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def pbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(accountConfig)]) + + and: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + } + + def "PBS auction should include include default price granularity when original request and account config doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include default price granularity" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.default + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(priceGranularity: UNKNOWN)] + } + + def "PBS amp should throw error when price granularity from original request is empty"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest with empty price granularity" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS amp should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + setAccountId(ampRequest.account) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, priceGranularity) + accountDao.save(account) + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + return new Account(uuid: accountId, config: accountConfig) } - private PrebidServerService getEnabledWinBidsPbsService() { + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } - private PrebidServerService getDisabledWinBidsPbsService() { + private static PrebidServerService getDisabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "false"]) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy index d04808f4fa2..99fa3b31a0e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy @@ -1,11 +1,9 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.PrebidStoredRequest -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.PBSUtils @@ -17,7 +15,6 @@ import static org.prebid.server.functional.testcontainers.container.PrebidServer class TimeoutSpec extends BaseSpec { private static final int DEFAULT_TIMEOUT = getRandomTimeout() - private static final int MIN_TIMEOUT_BIDDER_REQUEST = 5 private static final int MIN_TIMEOUT = PBSUtils.getRandomNumber(50, 150) private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, "auction.biddertmax.min" : MIN_TIMEOUT as String] @@ -284,76 +281,6 @@ class TimeoutSpec extends BaseSpec { assert isInternalProcessingTime(bidderRequest.tmax, MAX_TIMEOUT) } - def "PBS amp should return error when auction.biddertmax.min value not enough for bidder request"() { - given: "PBS config with biddertmax.min" - def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.min" : MIN_TIMEOUT_BIDDER_REQUEST as String]) - - and: "Default AMP request without timeout" - def ampRequest = AmpRequest.defaultAmpRequest.tap { - timeout = null - } - - and: "Default stored request tmax" - def minTmax = MIN_TIMEOUT_BIDDER_REQUEST - 1 - def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - tmax = minTmax - } - - and: "Save storedRequest into DB" - def storedRequestModel = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequestModel) - - when: "PBS processes amp request" - def bidResponse = prebidServerService.sendAmpRequest(ampRequest) - - then: "Bidder request timeout should correspond to the min from the stored request" - assert bidResponse?.ext?.debug?.resolvedRequest?.tmax == minTmax - - and: "PBS should send to bidder tmax form auction.biddertmax.min config" - assert bidResponse.ext.debug.httpcalls[BidderName.GENERIC.value]*.requestBody[0].contains("\"tmax\":${MIN_TIMEOUT_BIDDER_REQUEST}") - - and: "Bid response should shutdown by timeout from stored request" - def errors = bidResponse.ext?.errors - assert errors[ErrorType.GENERIC]*.code == [1] - assert errors[ErrorType.GENERIC]*.message == ["Timeout has been exceeded"] - } - - def "PBS auction should return error when auction.biddertmax.min value not enough for bidder request"() { - given: "PBS config with biddertmax.min" - def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.max" : MAX_TIMEOUT as String, - "auction.biddertmax.min" : MIN_TIMEOUT_BIDDER_REQUEST as String]) - - and: "Default BidRequest without timeout" - def bidRequest = BidRequest.defaultBidRequest.tap { - tmax = null - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) - } - - and: "Default stored request with min tmax" - def minTmax = MIN_TIMEOUT_BIDDER_REQUEST + 4 - def storedRequest = BidRequest.defaultStoredRequest.tap { - tmax = minTmax - } - - and: "Save storedRequest into DB" - def storedRequestModel = StoredRequest.getStoredRequest(bidRequest.ext.prebid.storedRequest.id, storedRequest) - storedRequestDao.save(storedRequestModel) - - when: "PBS processes auction request" - def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) - - then: "Bidder request timeout should correspond to the min from the stored request" - assert bidResponse?.ext?.debug?.resolvedRequest?.tmax == minTmax - - and: "PBS should send to bidder tmax form auction.biddertmax.min config" - assert bidResponse.ext.debug.httpcalls[BidderName.GENERIC.value]*.requestBody[0].contains("\"tmax\":${MIN_TIMEOUT_BIDDER_REQUEST}") - - and: "Bid response should shutdown by timeout from stored request" - def errors = bidResponse.ext?.errors - assert errors[ErrorType.GENERIC]*.code == [1] - assert errors[ErrorType.GENERIC]*.message == ["Timeout has been exceeded"] - } - def "PBS should choose min timeout form config for bidder request when in request value lowest that in auction.biddertmax.min"() { given: "PBS config with percent" def minBidderTmax = PBSUtils.getRandomNumber(MIN_TIMEOUT, MAX_TIMEOUT) diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 33a6d83b500..19cb2cd53de 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -4,6 +4,8 @@ import org.prebid.server.functional.model.config.Endpoint import org.prebid.server.functional.model.config.ExecutionPlan import org.prebid.server.functional.tests.BaseSpec +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES @@ -21,6 +23,12 @@ class ModuleBaseSpec extends BaseSpec { repository.removeAllDatabaseData() } + protected static Map getResponseCorrectionConfig(Endpoint endpoint = OPENRTB2_AUCTION) { + ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled" : true, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RESPONSE_CORRECTION, [ALL_PROCESSED_BID_RESPONSES]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getRichMediaFilterSettings(String scriptPattern, boolean filterMraidEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { @@ -28,7 +36,7 @@ class ModuleBaseSpec extends BaseSpec { ["hooks.${PB_RICHMEDIA_FILTER.code}.enabled" : true, "hooks.modules.${PB_RICHMEDIA_FILTER.code}.mraid-script-pattern": scriptPattern, "hooks.modules.${PB_RICHMEDIA_FILTER.code}.filter-mraid" : filterMraidEnabled, - "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RICHMEDIA_FILTER, ALL_PROCESSED_BID_RESPONSES))] + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]))] .collectEntries { key, value -> [(key.toString()): value.toString()] } } @@ -39,4 +47,8 @@ class ModuleBaseSpec extends BaseSpec { "hooks.modules.${PB_RICHMEDIA_FILTER.code}.filter-mraid" : filterMraidEnabled] .collectEntries { key, value -> [(key.toString()): value.toString()] } } + + protected static Map getOrtb2BlockingSettings(boolean isEnabled = true) { + ["hooks.${ORTB2_BLOCKING.code}.enabled": isEnabled as String] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy new file mode 100644 index 00000000000..8a99628b70c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy @@ -0,0 +1,296 @@ +package org.prebid.server.functional.tests.module.analyticstag + +import org.prebid.server.functional.model.config.AccountAnalyticsConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.AnalyticsOptions +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.FetchStatus +import org.prebid.server.functional.model.request.auction.PrebidAnalytics +import org.prebid.server.functional.model.request.auction.RichmediaFilter +import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ModuleActivityName +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID + +class AnalyticsTagsModuleSpec extends ModuleBaseSpec { + + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings) + + def "PBS should include analytics tag for ortb2-blocking module in response when request and account allow client details"() { + given: "Default account with module config" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: true)) + } + + and: "Account in the DB" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: true)) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "Bid response should contain ext.prebid.analyticsTags with module record" + def analyticsTagPrebid = bidResponse.ext.prebid.analytics.tags.first + assert analyticsTagPrebid.stage == RAW_BIDDER_RESPONSE.value + assert analyticsTagPrebid.module == ORTB2_BLOCKING.code + + and: "Analytics tag should contain results with name and success status" + def analyticResult = analyticsTagPrebid.analyticsTags.activities.first + assert analyticResult.status == FetchStatus.SUCCESS + assert analyticResult.name == ModuleActivityName.ORTB2_BLOCKING + + and: "Should include appliedTo information in analytics tags results" + verifyAll(analyticResult.results.first) { + it.status == FetchStatus.SUCCESS_ALLOW + it.appliedTo.bidders == [GENERIC.value] + it.appliedTo.impIds == bidRequest.imp.id + } + } + + def "PBS should include analytics tag for richmedia module in response when request and account allow client details"() { + given: "PBS server with enabled media filter" + def PATTERN_NAME = PBSUtils.randomString + def pbsServiceWithEnabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME)) + + and: "BidRequest with stored response" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.trace = VERBOSE + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: true)) + it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = PATTERN_NAME + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB with cofig" + def richmediaFilter = new RichmediaFilter(filterMraid: true, mraidScriptPattern: PATTERN_NAME) + def richMediaFilterConfig = new PbsModulesConfig(pbRichmediaFilter: richmediaFilter) + def accountHooksConfig = new AccountHooksConfiguration(modules: richMediaFilterConfig) + def accountAnalyticsConfig = new AccountAnalyticsConfig(allowClientDetails: true) + def accountConfig = new AccountConfig(hooks: accountHooksConfig, analytics: accountAnalyticsConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithEnabledMediaFilter.sendAuctionRequest(bidRequest) + + then: "Bid response should contain ext.prebid.analyticsTags with module record" + def analyticsTagPrebid = bidResponse.ext.prebid.analytics.tags.first + assert analyticsTagPrebid.stage == ALL_PROCESSED_BID_RESPONSES.value + assert analyticsTagPrebid.module == PB_RICHMEDIA_FILTER.code + + and: "Analytics tag should contain results with name and success status" + def analyticResult = analyticsTagPrebid.analyticsTags.activities.first + assert analyticResult.status == FetchStatus.SUCCESS + assert analyticResult.name == ModuleActivityName.REJECT_RICHMEDIA + + and: "Should include appliedTo information in analytics tags results" + verifyAll(analyticResult.results.first) { + it.status == FetchStatus.SUCCESS_BLOCK + it.appliedTo.bidders == [GENERIC.value] + it.appliedTo.impIds == bidRequest.imp.id + } + } + + def "PBS should include analytics tag in response when request and default account allow client details"() { + given: "Default account with module config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: true)) + + and: "Prebid server with proper default account" + def pbsConfig = ['settings.default-account-config': encode(accountConfig)] + ortb2BlockingSettings + def pbsServiceWithDefaultAccount = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with enabled client details" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: true)) + } + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithDefaultAccount.sendAuctionRequest(bidRequest) + + then: "Bid response should contain ext.prebid.analyticsTags with module record" + def analyticsTagPrebid = bidResponse.ext.prebid.analytics.tags.first + assert analyticsTagPrebid.stage == RAW_BIDDER_RESPONSE.value + assert analyticsTagPrebid.module == ORTB2_BLOCKING.code + + and: "Analytics tag should contain results with name and success status" + def analyticResult = analyticsTagPrebid.analyticsTags.activities.first + assert analyticResult.status == FetchStatus.SUCCESS + assert analyticResult.name == ModuleActivityName.ORTB2_BLOCKING + + and: "Should include appliedTo information in analytics tags results" + verifyAll(analyticResult.results.first) { + it.status == FetchStatus.SUCCESS_ALLOW + it.appliedTo.bidders == [GENERIC.value] + it.appliedTo.impIds == bidRequest.imp.id + } + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should include analytics tag in response when request and account allow client details but default doesn't"() { + given: "Default account with module config" + def defaultExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def defaultHooksConfiguration = new AccountHooksConfiguration(executionPlan: defaultExecutionPlan) + def defaultAccountConfig = new AccountConfig(hooks: defaultHooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: false)) + + and: "Prebid server with proper default account" + def pbsConfig = ['settings.default-account-config': encode(defaultAccountConfig)] + ortb2BlockingSettings + def pbsServiceWithDefaultAccount = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with enabled client details" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: true)) + } + + and: "Account in the DB" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: true)) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithDefaultAccount.sendAuctionRequest(bidRequest) + + then: "Bid response should contain ext.prebid.analyticsTags with module record" + def analyticsTagPrebid = bidResponse.ext.prebid.analytics.tags.first + assert analyticsTagPrebid.stage == RAW_BIDDER_RESPONSE.value + assert analyticsTagPrebid.module == ORTB2_BLOCKING.code + + and: "Analytics tag should contain results with name and success status" + def analyticResult = analyticsTagPrebid.analyticsTags.activities.first + assert analyticResult.status == FetchStatus.SUCCESS + assert analyticResult.name == ModuleActivityName.ORTB2_BLOCKING + + and: "Should include appliedTo information in analytics tags results" + verifyAll(analyticResult.results.first) { + it.status == FetchStatus.SUCCESS_ALLOW + it.appliedTo.bidders == [GENERIC.value] + it.appliedTo.impIds == bidRequest.imp.id + } + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should not include analytics tag in response without any warnings when timeout module disabled"() { + given: "Default account with module config" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: true)) + } + + and: "Account in the DB" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: true)) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain any analytics tag" + assert !bidResponse?.ext?.prebid?.analytics?.tags + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + } + + def "PBS should not include analytics tag in response without any warnings when client details is disabled in request"() { + given: "Default account with module config" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: false)) + } + + and: "Account in the DB" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: true)) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain any analytics tag" + assert !bidResponse?.ext?.prebid?.analytics?.tags + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + } + + def "PBS should not include analytics tag in response with warning when client details is disabled in account"() { + given: "Default account with module config" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: true)) + } + + and: "Account in the DB" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: false)) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain any analytics tag" + assert !bidResponse?.ext?.prebid?.analytics?.tags + + and: "Bid response should contain warning" + assert bidResponse.ext.warnings[PREBID]?.code == [999] + assert bidResponse.ext.warnings[PREBID]?.message == ["analytics.options.enableclientdetails not enabled for account"] + } + + def "PBS should not include analytics tag in response without warning when client details is disabled in account and request"() { + given: "Default account with module config" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.analytics = new PrebidAnalytics(options: new AnalyticsOptions(enableClientDetails: false)) + } + + and: "Account in the DB" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [RAW_BIDDER_RESPONSE]) + def hooksConfiguration = new AccountHooksConfiguration(executionPlan: executionPlan) + def accountConfig = new AccountConfig(hooks: hooksConfiguration, analytics: new AccountAnalyticsConfig(allowClientDetails: false)) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain any analytics tag" + assert !bidResponse?.ext?.prebid?.analytics?.tags + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy new file mode 100644 index 00000000000..b37cae6a067 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -0,0 +1,1511 @@ +package org.prebid.server.functional.tests.module.ortb2blocking + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingActionOverride +import org.prebid.server.functional.model.config.Ortb2BlockingAttributeConfig +import org.prebid.server.functional.model.config.Ortb2BlockingAttribute +import org.prebid.server.functional.model.config.Ortb2BlockingConditions +import org.prebid.server.functional.model.config.Ortb2BlockingConfig +import org.prebid.server.functional.model.config.Ortb2BlockingOverride +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.Asset +import org.prebid.server.functional.model.request.auction.Audio +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.Ix +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Video +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidMediaType +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.IX +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR +import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class Ortb2BlockingSpec extends ModuleBaseSpec { + + private static final Map IX_CONFIG = ["adapters.ix.enabled" : "true", + "adapters.ix.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final String WILDCARD = '*' + + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + IX_CONFIG + + ["adapters.generic.ortb.multiformat-supported": "true"]) + + def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS should be able to send original array ortb2 attribute to bidder alias"() { + given: "Default bid request with alias" + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + ext.prebid.aliases = [(ALIAS.value): GENERIC] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.alias = new Generic() + } + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [ortb2Attributes]*.toString() + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, ortb2Attributes, attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.prebid.modules.errors.ortb2Blocking["ortb2-blocking-bidder-request"].first + .contains("field in account configuration is not an array") + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request shouldn't contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !getOrtb2Attributes(bidderRequest, attributeName) + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.prebid.modules.errors.ortb2Blocking["ortb2-blocking-bidder-request"].first + .contains("field in account configuration has unexpected type") + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request shouldn't contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !getOrtb2Attributes(bidderRequest, attributeName) + + where: + ortb2Attributes | attributeName + PBSUtils.randomNumber | BADV + PBSUtils.randomNumber | BAPP + PBSUtils.randomNumber | BCAT + PBSUtils.randomString | BANNER_BATTR + PBSUtils.randomString | VIDEO_BATTR + PBSUtils.randomString | AUDIO_BATTR + PBSUtils.randomString | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should send only not matched ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName), + getBidWithOrtb2Attribute(bidRequest.imp.first, allowedOrtb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + allowedOrtb2Attributes | disallowedOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | BANNER_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | VIDEO_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | AUDIO_BATTR + } + + def "PBS should left only not matched ortb2 attribute to bidder with multiply type imp when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + banner = Banner.getDefaultBanner().tap { + battr = [PBSUtils.randomNumber] + } + video = Video.getDefaultVideo().tap { + battr = [PBSUtils.randomNumber] + } + audio = Audio.getDefaultAudio().tap { + battr = [PBSUtils.randomNumber] + } + ext.prebid.bidder.generic = null + ext.prebid.bidder.ix = Ix.default + } + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def disallowedOrtb2Attributes = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def removeBid = getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName).tap { + it.mediaType = enforceType + } + def presentBid = getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName).tap { + it.mediaType = presentType + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [removeBid, presentBid] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.mediaType == presentType + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [disallowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + attributeName | enforceType | presentType + BANNER_BATTR | BidMediaType.BANNER | BidMediaType.AUDIO + VIDEO_BATTR | BidMediaType.VIDEO | BidMediaType.BANNER + AUDIO_BATTR | BidMediaType.AUDIO | BidMediaType.VIDEO + } + + def "PBS should send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = false + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain proper seatbid" + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should discard unknown adomain bids when enforcement is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def allowedOrtb2Attributes = PBSUtils.randomString + def bidPrice = PBSUtils.randomPrice + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + price = bidPrice + 1 // to guarantee higher priority by default settings + } + def bidWithAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = [allowedOrtb2Attributes] + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain, bidWithAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, BADV) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should not discard unknown adomain bids when enforcement is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2BlockingAttributeConfig << [new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: false), + new Ortb2BlockingAttributeConfig(enforceBlocks: false, blockUnknownAdomain: true), + new Ortb2BlockingAttributeConfig(enforceBlocks: true)] + } + + def "PBS should discard unknown adv cat bids when enforcement is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def allowedOrtb2Attributes = PBSUtils.randomString + def bidPrice = PBSUtils.randomPrice + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + price = bidPrice + 1 // to guarantee higher priority by default settings + } + def bidWithAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = [allowedOrtb2Attributes] + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain, bidWithAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, BCAT) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should not discard unknown adv cat bids when enforcement is disabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2BlockingAttributeConfig << [new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: false), + new Ortb2BlockingAttributeConfig(enforceBlocks: false, blockUnknownAdvCat: true), + new Ortb2BlockingAttributeConfig(enforceBlocks: true)] + } + + def "PBS should not discard bids with deals when allowed ortb2 attribute for deals is matched"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def attributes = [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2Attributes]).tap { + enforceBlocks = true + }] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = PBSUtils.randomNumber }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should discard bids with deals when allowed ortb2 attribute for deals is not matched"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def attributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([allowedOrtb2Attributes, dielsOrtb2Attributes], attributeName, [allowedOrtb2Attributes]).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): attributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, dielsOrtb2Attributes, attributeName) + .tap { dealid = PBSUtils.randomNumber }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid.bid.flatten().size() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + allowedOrtb2Attributes | dielsOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override enforcement by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC), + new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: IX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only openx seatbid" + assert response.seatbid.size() == 1 + assert response.seatbid.first.seat == IX + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override enforcement by media type"() { + given: "Bid request with multy type imp" + def bannerImp = Imp.getDefaultImpression(BANNER) + def videoImp = Imp.getDefaultImpression(VIDEO) + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + imp = [bannerImp, videoImp] + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bannerImp, ortb2Attributes, attributeName)]), + new SeatBid(bid: [getBidWithOrtb2Attribute(videoImp, ortb2Attributes, attributeName)])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.impid == bannerImp.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + } + + def "PBS should be able to override enforcement by media type for battr attribute"() { + given: "Default bid request with proper ortb attribute" + BidRequest bidRequest = getBidRequestForOrtbAttribute(attributeName, [PBSUtils.randomNumber]).tap { +// default resolve for bids always prefer type from request, ix from response and only then from request if null + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [mediaType]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bid = getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName).tap { + it.mediaType = bidMediaType + it.adm = new Adm(assets: [Asset.defaultAsset]) // required for video type + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bid])] + + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.impid == bidRequest.imp.first.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attribute]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + attributeName | mediaType | bidMediaType + BANNER_BATTR | BANNER | null + VIDEO_BATTR | VIDEO | null + AUDIO_BATTR | AUDIO | null + BANNER_BATTR | BANNER | BidMediaType.BANNER + VIDEO_BATTR | VIDEO | BidMediaType.VIDEO + AUDIO_BATTR | AUDIO | BidMediaType.AUDIO + BANNER_BATTR | BANNER | BidMediaType.AUDIO + VIDEO_BATTR | VIDEO | BidMediaType.BANNER + AUDIO_BATTR | AUDIO | BidMediaType.VIDEO + } + + def "PBS shouldn't be able to override enforcement by incorrect media type for battr attribute"() { + given: "Default bid request with proper ortb attribute" + BidRequest bidRequest = getBidRequestForOrtbAttribute(attributeName, [PBSUtils.randomNumber]).tap { + // default resolve for bids always prefer type from request, ix from response and only then from request if null + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [mediaType]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bid = getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName).tap { + it.mediaType = bidMediaType + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bid])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid.bid.flatten().size() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request should contain original ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == getOrtb2Attributes(bidRequest, attributeName) + + where: + attributeName | mediaType | bidMediaType + BANNER_BATTR | AUDIO | null + VIDEO_BATTR | BANNER | null + AUDIO_BATTR | VIDEO | null + } + + def "PBS should be able to override enforcement by deal id"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: new Ortb2BlockingConditions(dealIds: [dealId.toString()])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2AttributesForDeals]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [blockingCondition]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + dealId | ortb2Attributes | ortb2AttributesForDeals | attributeName + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BADV + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BAPP + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BCAT + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [GENERIC]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [overrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [ortb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [overrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | overrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [overrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [ortb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [overrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | overrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + } + + def "PBS should be able to override block unknown adomain by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV).tap { + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdomain: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutAdomain], seat: GENERIC), + new SeatBid(bid: [bidWithOutAdomain], seat: IX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only ix seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.seat == IX + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adomain by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdomain: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutAdomain])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adv-cat by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT).tap { + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdvCat: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutCat = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutCat], seat: GENERIC), + new SeatBid(bid: [bidWithOutCat], seat: IX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only ix seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.seat == IX + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adv-cat by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdvCat: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutCat = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutCat])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override allowed ortb2 attribute for deals by deal ids"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def dealId = PBSUtils.randomNumber + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [dealOverrideAttributes]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [ortb2BlockingOverride]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | dealOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should use first override when multiple match same condition"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: blockingCondition) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [firstOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response should contain proper warning" + assert response?.ext?.prebid?.modules?.warnings?.ortb2Blocking["ortb2-blocking-bidder-request"] == + ["More than one conditions matches request. Bidder: generic, request media types: [${bidRequest.imp[0].mediaTypes[0].value}]"] + + where: + blockingCondition | ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + new Ortb2BlockingConditions(mediaType: [VIDEO]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + new Ortb2BlockingConditions(mediaType: [AUDIO]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by bidder"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [BidderName.WILDCARD])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [GENERIC])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [secondOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by media type"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [MediaType.WILDCARD])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [bidRequest.imp[0].mediaTypes[0]])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [secondOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + } + + def "PBS should merge allowed bundle for deals overrides together"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) + + and: "Account in the DB with blocking configuration" + def dealId = PBSUtils.randomNumber + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [ortb2Attributes.last], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig(ortb2Attributes, attributeName, [ortb2Attributes.first]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [ortb2BlockingOverride]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == ortb2Attributes*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + [PBSUtils.randomString, PBSUtils.randomString] | BADV + [PBSUtils.randomString, PBSUtils.randomString] | BCAT + [PBSUtils.randomNumber, PBSUtils.randomNumber] | BANNER_BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | VIDEO_BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | AUDIO_BATTR + } + + def "PBS should not be override from config when ortb2 attribute present in incoming request"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName, bidRequestAttribute) + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain original ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == getOrtb2Attributes(bidRequest, attributeName) + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + bidRequestAttribute | ortb2Attributes | attributeName + [PBSUtils.randomString] | PBSUtils.randomString | BADV + [PBSUtils.randomString] | PBSUtils.randomString | BAPP + [PBSUtils.randomString] | PBSUtils.randomString | BCAT + [PBSUtils.randomNumber] | PBSUtils.randomNumber | BANNER_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | VIDEO_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | AUDIO_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | BTYPE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with rejected advertiser blocked status code"() { + given: "Default bidRequest with returnAllBidStatus attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV).tap { + it.ext.prebid.returnAllBidStatus = true + } + + and: "Default bidder response with aDomain" + def aDomain = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, aDomain, BADV)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with blocking configuration" + def attributes = [(BADV): new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockedAdomain: [aDomain])] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) + accountDao.save(account) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == ErrorType.GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED + } + + private static Account getAccountWithOrtb2BlockingConfig(String accountId, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { + getAccountWithOrtb2BlockingConfig(accountId, [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig(ortb2Attributes, attributeName)]) + } + + private static Account getAccountWithOrtb2BlockingConfig(String accountId, Map attributes) { + def blockingConfig = new Ortb2BlockingConfig(attributes: attributes) + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [BIDDER_REQUEST, RAW_BIDDER_RESPONSE]) + def moduleConfig = new PbsModulesConfig(ortb2Blocking: blockingConfig) + def accountHooksConfig = new AccountHooksConfiguration(executionPlan: executionPlan, modules: moduleConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: accountId, config: accountConfig) + } + + private static BidRequest getBidRequestForOrtbAttribute(Ortb2BlockingAttribute attribute, List attributeValue = null) { + switch (attribute) { + case BADV: + return BidRequest.defaultBidRequest.tap { + badv = attributeValue as List + } + case BAPP: + return BidRequest.defaultBidRequest.tap { + bapp = attributeValue as List + } + case BANNER_BATTR: + return BidRequest.defaultBidRequest.tap { + imp[0].banner.battr = attributeValue as List + } + case VIDEO_BATTR: + return BidRequest.defaultVideoRequest.tap { + imp[0].video.battr = attributeValue as List + } + case AUDIO_BATTR: + return BidRequest.defaultAudioRequest.tap { + imp[0].audio.battr = attributeValue as List + } + case BCAT: + return BidRequest.defaultBidRequest.tap { + bcat = attributeValue as List + } + case BTYPE: + return BidRequest.defaultBidRequest.tap { + imp[0].banner.btype = attributeValue as List + } + default: + throw new IllegalArgumentException("Unknown ortb2 attribute: $attribute") + } + } + + private static Bid getBidWithOrtb2Attribute(Imp imp, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { + Bid.getDefaultBid(imp).tap { + switch (attributeName) { + case BADV: + adomain = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BAPP: + bundle = (ortb2Attributes instanceof List) ? ortb2Attributes.first : ortb2Attributes + break + case BANNER_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case VIDEO_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case AUDIO_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BCAT: + cat = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BTYPE: + break + default: + throw new IllegalArgumentException("Unknown ortb2 attribute: $attributeName") + } + } + } + + private static List getOrtb2Attributes(BidRequest bidRequest, Ortb2BlockingAttribute attributeName) { + switch (attributeName) { + case BADV: + return bidRequest.badv + case BAPP: + return bidRequest.bapp + case BANNER_BATTR: + return bidRequest.imp[0].banner.battr*.toString() + case VIDEO_BATTR: + return bidRequest.imp[0].video.battr*.toString() + case AUDIO_BATTR: + return bidRequest.imp[0].audio.battr*.toString() + case BCAT: + return bidRequest.bcat + case BTYPE: + return bidRequest.imp[0].banner.btype*.toString() + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } + + private static List getOrtb2Attributes(Bid bid, Ortb2BlockingAttribute attributeName) { + switch (attributeName) { + case BADV: + return bid.adomain + case BAPP: + return [bid.bundle] + case BANNER_BATTR: + return bid.attr*.toString() + case VIDEO_BATTR: + return bid.attr*.toString() + case AUDIO_BATTR: + return bid.attr*.toString() + case BCAT: + return bid.cat + case BTYPE: + return null + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy new file mode 100644 index 00000000000..c848c30e2e4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy @@ -0,0 +1,581 @@ +package org.prebid.server.functional.tests.module.responsecorrenction + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AppVideoHtml +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.Meta +import org.prebid.server.functional.model.response.auction.Prebid +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultVideoRequest +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class ResponseCorrectionSpec extends ModuleBaseSpec { + + private final PrebidServerService pbsServiceWithResponseCorrectionModule = pbsServiceFactory.getService( + ["adapter-defaults.modifying-vast-xml-allowed": "false", + "adapters.generic.modifying-vast-xml-allowed": "false"] + + responseCorrectionConfig) + + private final static int OPTIMAL_MAX_LENGTH = 20 + + def "PBS shouldn't modify response when in account correction module disabled"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest, responseCorrectionEnabled, appVideoHtmlEnabled) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + responseCorrectionEnabled | appVideoHtmlEnabled + false | true + true | false + false | false + } + + def "PBS shouldn't modify response with adm obj when request includes #distributionChannel distribution channel"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request video imp" + def bidRequest = getDefaultVideoRequest(distributionChannel) + + and: "Set bidder response with adm obj" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + distributionChannel << [SITE, DOOH] + } + + def "PBS shouldn't modify response for excluded bidder when bidder specified in config"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module and excluded bidders" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest).tap { + config.hooks.modules.pbResponseCorrection.appVideoHtml.excludedBidders = [GENERIC] + } + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response and emit warning when requested video impression respond with adm without VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response without adm obj when request includes #mediaType media type"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [mediaType] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType << [BANNER, AUDIO] + } + + def "PBS shouldn't modify response and emit logs when requested impression with native and adm value is asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + assert responseCorrection.size() == 1 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [NATIVE] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with empty adm"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(null) + seatbid[0].bid[0].nurl = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + admValue << [ + "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}", + "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}>", + "${PBSUtils.randomString}${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}>", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}>", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}>" + ] + } + + def "PBS should modify response when requested video impression respond with invalid adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + admValue << [ + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST>", + "<${PBSUtils.randomString}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + ] + } + + def "PBS should modify response when requested #mediaType impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType | admValue + BANNER | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + BANNER | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + BANNER | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + AUDIO | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + AUDIO | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + AUDIO | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + NATIVE | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + NATIVE | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + NATIVE | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + } + + def "PBS shouldn't modify response meta.mediaType to video and emit logs when requested impression with video and adm obj with asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and audio imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + } + + and: "Response should contain seatBib" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS should modify meta.mediaType and type for original response and also emit logs when response contains native meta.mediaType and adm without asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + seatbid[0].bid[0].ext = new BidExt(prebid: new Prebid(meta: new Meta(mediaType: NATIVE))) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 2 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has a JSON ADM, but without assets" as String) + } + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should media type for prebid meta" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + private static Account accountConfigWithResponseCorrectionModule(BidRequest bidRequest, Boolean enabledResponseCorrection = true, Boolean enabledAppVideoHtml = true) { + def modulesConfig = new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection( + enabled: enabledResponseCorrection, appVideoHtml: new AppVideoHtml(enabled: enabledAppVideoHtml))) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy index d20d4f51dd4..b12ff9644b4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy @@ -2,6 +2,8 @@ package org.prebid.server.functional.tests.module.richmedia import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.HookId import org.prebid.server.functional.model.config.PbsModulesConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredResponse @@ -10,12 +12,15 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.AnalyticResult import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE +import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE class RichMediaFilterSpec extends ModuleBaseSpec { @@ -23,12 +28,18 @@ class RichMediaFilterSpec extends ModuleBaseSpec { private static final String PATTERN_NAME = PBSUtils.randomString private static final String PATTERN_NAME_ACCOUNT = PBSUtils.randomString private final PrebidServerService pbsServiceWithEnabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME)) + private final PrebidServerService pbsServiceWithEnabledMediaFilterAndDifferentCaseStrategy = pbsServiceFactory.getService( + (getRichMediaFilterSettings(PATTERN_NAME) + ["hooks.host-execution-plan": encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]).tap { + endpoints.values().first().stages.values().first().groups.first.hookSequenceSnakeCase = [new HookId(moduleCodeSnakeCase: PB_RICHMEDIA_FILTER.code, hookImplCodeSnakeCase: "${PB_RICHMEDIA_FILTER.code}-${ALL_PROCESSED_BID_RESPONSES.value}-hook")] + })]) + .collectEntries { key, value -> [(key.toString()): value.toString()] }) private final PrebidServerService pbsServiceWithDisabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME, false)) def "PBS should process request without analytics when adm matches with pattern name and filter set to disabled in host config"() { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -64,6 +75,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -86,10 +98,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -105,6 +119,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -142,6 +157,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -166,10 +182,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -185,6 +203,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -222,6 +241,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -246,10 +266,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -262,6 +284,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -299,6 +322,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { and: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -332,9 +356,53 @@ class RichMediaFilterSpec extends ModuleBaseSpec { admValue << [PATTERN_NAME, PATTERN_NAME_ACCOUNT] } + def "PBS should reject request with error and provide analytic when adm matches with pattern name and filter set to enabled in host config with different name case"() { + given: "BidRequest with stored response" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true + it.ext.prebid.trace = VERBOSE + it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = admValue as String + } + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.getAccountId()) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithEnabledMediaFilterAndDifferentCaseStrategy.sendAuctionRequest(bidRequest) + + then: "Response header shouldn't contain any seatbid" + assert !response.seatbid + + and: "Response should contain error of invalid creation for imp with code 350" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE + + and: "Add an entry to the analytics tag for this rejected bid response" + def analyticsTags = getAnalyticResults(response) + assert analyticsTags.size() == 1 + def analyticResult = analyticsTags.first() + assert analyticResult == AnalyticResult.buildFromImp(bidRequest.imp.first()) + + where: + admValue << [PATTERN_NAME, "${PBSUtils.randomString}-${PATTERN_NAME}", "${PATTERN_NAME}.${PBSUtils.randomString}"] + } + private static List getAnalyticResults(BidResponse response) { response.ext.prebid.modules?.trace?.stages?.first() ?.outcomes?.first()?.groups?.first() - ?.invocationResults?.first()?.analyticStags?.activities + ?.invocationResults?.first()?.analyticsTags?.activities } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/AlertSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/AlertSpec.groovy deleted file mode 100644 index bae95e69d96..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/AlertSpec.groovy +++ /dev/null @@ -1,281 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.mockserver.matchers.Times -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Retry - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static java.time.ZoneOffset.UTC -import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 -import static org.mockserver.model.HttpStatusCode.NOT_FOUND_404 -import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 -import static org.prebid.server.functional.model.deals.alert.Action.RAISE -import static org.prebid.server.functional.model.deals.alert.AlertPriority.LOW -import static org.prebid.server.functional.model.deals.alert.AlertPriority.MEDIUM -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME -import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER -import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER -import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER_VALUE -import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER -import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX - -class AlertSpec extends BasePgSpec { - - private static final String PBS_REGISTER_CLIENT_ERROR = "pbs-register-client-error" - private static final String PBS_PLANNER_CLIENT_ERROR = "pbs-planner-client-error" - private static final String PBS_PLANNER_EMPTY_RESPONSE = "pbs-planner-empty-response-error" - private static final String PBS_DELIVERY_CLIENT_ERROR = "pbs-delivery-stats-client-error" - private static final Integer DEFAULT_ALERT_PERIOD = 15 - - @Retry(exceptions = [IllegalStateException.class]) - def "PBS should send alert request when the threshold is reached"() { - given: "Changed Planner Register endpoint response to return bad status code" - generalPlanner.initRegisterResponse(NOT_FOUND_404) - - and: "PBS alert counter is reset" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) - - and: "Initial Alert Service request count is taken" - def initialRequestCount = alert.requestCount - - when: "Initiating PBS to register its instance through the bad Planner for the first time" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "PBS sends an alert request to the Alert Service for the first time" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } - - when: "Initiating PBS to register its instance through the bad Planner until the period threshold of alerts is reached" - (2..DEFAULT_ALERT_PERIOD).forEach { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - } - - then: "PBS sends an alert request to the Alert Service for the second time" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 2 } - - and: "Request has the right number of failed register attempts" - def alertRequest = alert.recordedAlertRequest - assert alertRequest.details.startsWith("Service register failed to send request $DEFAULT_ALERT_PERIOD " + - "time(s) with error message") - - cleanup: "Return initial Planner response status code" - generalPlanner.initRegisterResponse() - } - - def "PBS should send an alert request with appropriate headers"() { - given: "Changed Planner Register endpoint response to return bad status code" - generalPlanner.initRegisterResponse(NOT_FOUND_404) - - and: "PBS alert counter is reset" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) - - and: "Initial Alert Service request count is taken" - def initialRequestCount = alert.requestCount - - when: "Initiating PBS to register its instance through the bad Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - and: "PBS sends an alert request to the Alert Service" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } - - then: "Request headers correspond to the payload" - def alertRequestHeaders = alert.lastRecordedAlertRequestHeaders - assert alertRequestHeaders - - and: "Request has an authorization header with a basic auth token" - def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) - assert alertRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] - - and: "Request has a header with uuid value" - def uuidHeaderValue = alertRequestHeaders.get(PG_TRX_ID_HEADER) - assert uuidHeaderValue?.size() == 1 - assert (uuidHeaderValue[0] =~ UUID_REGEX).matches() - - and: "Request has a content type header" - assert alertRequestHeaders.get(CONTENT_TYPE_HEADER) == [CONTENT_TYPE_HEADER_VALUE] - - cleanup: "Return initial Planner response status code" - generalPlanner.initRegisterResponse() - } - - def "PBS should send an alert when fetching line items response status wasn't OK ('#httpStatusCode')"() { - given: "Changed Planner line items endpoint response to return bad status code" - // PBS will make 2 requests to the planner: 1 normal, 2 - recovery request - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString), httpStatusCode, Times.exactly(2)) - - and: "PBS alert counter is reset" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) - - and: "Initial Alert Service request count is taken" - def initialRequestCount = alert.requestCount - - when: "Initiating PBS to fetch line items through the bad Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - - then: "PBS sends an alert request to the Alert Service" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } - - and: "Alert request should correspond to the payload" - verifyAll(alert.recordedAlertRequest) { alertRequest -> - (alertRequest.id =~ UUID_REGEX).matches() - alertRequest.action == RAISE - alertRequest.priority == MEDIUM - alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) - alertRequest.name == PBS_PLANNER_CLIENT_ERROR - alertRequest.details == "Service planner failed to send request 1 time(s) with error message :" + - " Failed to retrieve line items from GP. Reason: Failed to fetch data from Planner, HTTP status code ${httpStatusCode.code()}" - - alertRequest.source.env == pgConfig.env - alertRequest.source.dataCenter == pgConfig.dataCenter - alertRequest.source.region == pgConfig.region - alertRequest.source.system == pgConfig.system - alertRequest.source.subSystem == pgConfig.subSystem - alertRequest.source.hostId == pgConfig.hostId - } - - cleanup: "Return initial Planner response status code" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) - - where: "Bad status codes" - httpStatusCode << [NO_CONTENT_204, NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] - } - - def "PBS should send an alert when register PBS instance response status wasn't OK ('#httpStatusCode')"() { - given: "Changed Planner register endpoint response to return bad status code" - generalPlanner.initRegisterResponse(httpStatusCode) - - and: "PBS alert counter is reset" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) - - and: "Initial Alert Service request count is taken" - def initialRequestCount = alert.requestCount - - when: "Initiating PBS to register its instance through the bad Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "PBS sends an alert request to the Alert Service" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } - - and: "Alert request should correspond to the payload" - verifyAll(alert.recordedAlertRequest) { alertRequest -> - (alertRequest.id =~ UUID_REGEX).matches() - alertRequest.action == RAISE - alertRequest.priority == MEDIUM - alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) - alertRequest.name == PBS_REGISTER_CLIENT_ERROR - alertRequest.details.startsWith("Service register failed to send request 1 time(s) with error message :" + - " Planner responded with non-successful code ${httpStatusCode.code()}") - - alertRequest.source.env == pgConfig.env - alertRequest.source.dataCenter == pgConfig.dataCenter - alertRequest.source.region == pgConfig.region - alertRequest.source.system == pgConfig.system - alertRequest.source.subSystem == pgConfig.subSystem - alertRequest.source.hostId == pgConfig.hostId - } - - cleanup: "Return initial Planner response status code" - generalPlanner.initRegisterResponse() - - where: "Bad status codes" - httpStatusCode << [NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] - } - - def "PBS should send an alert when send delivery statistics report response status wasn't OK ('#httpStatusCode')"() { - given: "Changed Delivery Statistics endpoint response to return bad status code" - deliveryStatistics.reset() - deliveryStatistics.setResponse(httpStatusCode) - - and: "Set line items response" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) - - and: "PBS alert counter is reset" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) - - and: "Initial Alert Service request count is taken" - def initialRequestCount = alert.requestCount - - and: "Report to send is generated by PBS" - updateLineItemsAndWait() - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - when: "Initiating PBS to send delivery statistics report through the bad Delivery Statistics Service" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends an alert request to the Alert Service" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } - - and: "Alert request should correspond to the payload" - verifyAll(alert.recordedAlertRequest) { alertRequest -> - (alertRequest.id =~ UUID_REGEX).matches() - alertRequest.action == RAISE - alertRequest.priority == MEDIUM - alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) - alertRequest.name == PBS_DELIVERY_CLIENT_ERROR - alertRequest.details.startsWith("Service deliveryStats failed to send request 1 time(s) with error message : " + - "Report was not send to delivery stats service with a reason: Delivery stats service responded with " + - "status code = ${httpStatusCode.code()} for report with id = ") - - alertRequest.source.env == pgConfig.env - alertRequest.source.dataCenter == pgConfig.dataCenter - alertRequest.source.region == pgConfig.region - alertRequest.source.system == pgConfig.system - alertRequest.source.subSystem == pgConfig.subSystem - alertRequest.source.hostId == pgConfig.hostId - } - - cleanup: "Return initial Delivery Statistics response status code" - deliveryStatistics.reset() - deliveryStatistics.setResponse() - - and: "Report to delivery statistics is sent" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - where: "Bad status codes" - httpStatusCode << [NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] - } - - def "PBS should send an alert when Planner returns empty response"() { - given: "Changed Planner get plans response to return no plans" - generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) - - and: "PBS alert counter is reset" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) - - and: "Initial Alert Service request count is taken" - def initialRequestCount = alert.requestCount - - when: "Initiating PBS to fetch line items through the Planner" - updateLineItemsAndWait() - - then: "PBS sends an alert request to the Alert Service" - PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } - - and: "Alert request should correspond to the payload" - verifyAll(alert.recordedAlertRequest) { alertRequest -> - (alertRequest.id =~ UUID_REGEX).matches() - alertRequest.action == RAISE - alertRequest.priority == LOW - alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) - alertRequest.name == PBS_PLANNER_EMPTY_RESPONSE - alertRequest.details.startsWith("Service planner failed to send request 1 time(s) with error message : " + - "Response without line items was received from planner") - - alertRequest.source.env == pgConfig.env - alertRequest.source.dataCenter == pgConfig.dataCenter - alertRequest.source.region == pgConfig.region - alertRequest.source.system == pgConfig.system - alertRequest.source.subSystem == pgConfig.subSystem - alertRequest.source.hostId == pgConfig.hostId - } - - cleanup: "Return initial Planner response" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/BasePgSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/BasePgSpec.groovy deleted file mode 100644 index 9159bba8bb0..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/BasePgSpec.groovy +++ /dev/null @@ -1,60 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.Dependencies -import org.prebid.server.functional.testcontainers.PbsPgConfig -import org.prebid.server.functional.testcontainers.PbsServiceFactory -import org.prebid.server.functional.testcontainers.scaffolding.Bidder -import org.prebid.server.functional.testcontainers.scaffolding.pg.Alert -import org.prebid.server.functional.testcontainers.scaffolding.pg.DeliveryStatistics -import org.prebid.server.functional.testcontainers.scaffolding.pg.GeneralPlanner -import org.prebid.server.functional.testcontainers.scaffolding.pg.UserData -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Retry -import spock.lang.Shared -import spock.lang.Specification - -@Retry(mode = Retry.Mode.SETUP_FEATURE_CLEANUP) -abstract class BasePgSpec extends Specification { - - protected static final PbsServiceFactory pbsServiceFactory = new PbsServiceFactory(Dependencies.networkServiceContainer) - - protected static final GeneralPlanner generalPlanner = new GeneralPlanner(Dependencies.networkServiceContainer) - protected static final DeliveryStatistics deliveryStatistics = new DeliveryStatistics(Dependencies.networkServiceContainer) - protected static final Alert alert = new Alert(Dependencies.networkServiceContainer) - protected static final UserData userData = new UserData(Dependencies.networkServiceContainer) - - protected static final PbsPgConfig pgConfig = new PbsPgConfig(Dependencies.networkServiceContainer) - protected static final Bidder bidder = new Bidder(Dependencies.networkServiceContainer) - - @Shared - protected final PrebidServerService pgPbsService = pbsServiceFactory.getService(pgConfig.properties) - - def setupSpec() { - bidder.setResponse() - generalPlanner.setResponse() - deliveryStatistics.setResponse() - alert.setResponse() - userData.setResponse() - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) - } - - def cleanupSpec() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - generalPlanner.reset() - deliveryStatistics.reset() - alert.reset() - userData.reset() - bidder.reset() - } - - protected void updateLineItemsAndWait() { - def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/CurrencySpec.groovy deleted file mode 100644 index 9f2d6a3f540..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/CurrencySpec.groovy +++ /dev/null @@ -1,98 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.lineitem.LineItem -import org.prebid.server.functional.model.deals.lineitem.Price -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Shared - -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer - -class CurrencySpec extends BasePgSpec { - - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.defaultCurrencyConversionRatesResponse) - } - - private static final Map pgCurrencyConverterPbsConfig = externalCurrencyConverterConfig + pgConfig.properties - private static final PrebidServerService pgCurrencyConverterPbsService = pbsServiceFactory.getService(pgCurrencyConverterPbsConfig) - - @Shared - BidRequest bidRequest - - def setup() { - bidRequest = BidRequest.defaultBidRequest - bidder.setResponse(bidRequest.id, BidResponse.getDefaultBidResponse(bidRequest)) - pgCurrencyConverterPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should convert non-default line item currency to the default one during the bidder auction"() { - given: "Planner Mock line items with the same CPM but different currencies" - def accountId = bidRequest.site.publisher.id - def defaultCurrency = Price.defaultPrice.currency - def nonDefaultCurrency = "EUR" - def defaultCurrencyLineItem = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: defaultCurrency) }] - def nonDefaultCurrencyLineItems = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: nonDefaultCurrency) }, - LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: nonDefaultCurrency) }, - LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: nonDefaultCurrency) }] - def lineItems = defaultCurrencyLineItem + nonDefaultCurrencyLineItems - def plansResponse = new PlansResponse(lineItems: lineItems) - generalPlanner.initPlansResponse(plansResponse) - def nonDefaultCurrencyLineItemIds = nonDefaultCurrencyLineItems.collect { it.lineItemId } - - and: "Line items are fetched by PBS" - def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount - pgCurrencyConverterPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } - - when: "Auction is requested" - def auctionResponse = pgCurrencyConverterPbsService.sendAuctionRequest(bidRequest) - - then: "All line items are ready to be served" - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == plansResponse.lineItems.size() - - and: "Line Item with EUR defaultCurrency was sent to bidder as EUR defaultCurrency rate > than USD" - assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value)?.sort() == - nonDefaultCurrencyLineItemIds.sort() - } - - def "PBS should invalidate line item with an unknown for the conversion rate currency"() { - given: "Planner Mock line items with a default currency and unknown currency" - def defaultCurrency = Price.defaultPrice.currency - def unknownCurrency = "UAH" - def defaultCurrencyLineItem = [LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { price = new Price(cpm: 1, currency: defaultCurrency) }] - def unknownCurrencyLineItem = [LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { price = new Price(cpm: 1, currency: unknownCurrency) }] - def lineItems = defaultCurrencyLineItem + unknownCurrencyLineItem - def plansResponse = new PlansResponse(lineItems: lineItems) - generalPlanner.initPlansResponse(plansResponse) - def defaultCurrencyLineItemId = defaultCurrencyLineItem.collect { it.lineItemId } - - and: "Line items are fetched by PBS" - def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount - pgCurrencyConverterPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } - - when: "Auction is requested" - def auctionResponse = pgCurrencyConverterPbsService.sendAuctionRequest(bidRequest) - - then: "Only line item with the default currency is ready to be served and was sent to bidder" - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe == defaultCurrencyLineItemId as Set - assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) == - defaultCurrencyLineItemId as Set - } - - private static Map getExternalCurrencyConverterConfig() { - ["currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000" - ] - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/LineItemStatusSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/LineItemStatusSpec.groovy deleted file mode 100644 index 77f2c31f603..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/LineItemStatusSpec.groovy +++ /dev/null @@ -1,193 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.lineitem.DeliverySchedule -import org.prebid.server.functional.model.deals.lineitem.LineItem -import org.prebid.server.functional.model.deals.lineitem.Token -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.service.PrebidServerException -import org.prebid.server.functional.util.ObjectMapperWrapper -import org.prebid.server.functional.util.PBSUtils - -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit - -import static java.time.ZoneOffset.UTC - -class LineItemStatusSpec extends BasePgSpec implements ObjectMapperWrapper { - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should return a bad request exception when no 'id' query parameter is provided"() { - when: "Requesting endpoint without 'id' parameter" - pgPbsService.sendLineItemStatusRequest(null) - - then: "PBS throws an exception" - def exception = thrown(PrebidServerException) - assert exception.statusCode == 400 - assert exception.responseBody.contains("id parameter is required") - } - - def "PBS should return a bad request exception when endpoint is requested with not existing line item id"() { - given: "Not existing line item id" - def notExistingLineItemId = PBSUtils.randomString - - when: "Requesting endpoint" - pgPbsService.sendLineItemStatusRequest(notExistingLineItemId) - - then: "PBS throws an exception" - def exception = thrown(PrebidServerException) - assert exception.statusCode == 400 - assert exception.responseBody.contains("LineItem not found: $notExistingLineItemId") - } - - def "PBS should return an empty line item status response when line item doesn't have a scheduled delivery"() { - given: "Line item with no delivery schedule" - def plansResponse = PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].deliverySchedules = null - } - def lineItemId = plansResponse.lineItems[0].lineItemId - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Requesting endpoint" - def lineItemStatusReport = pgPbsService.sendLineItemStatusRequest(lineItemId) - - then: "Empty line item status report is returned" - verifyAll(lineItemStatusReport) { - it.lineItemId == lineItemId - !it.deliverySchedule - !it.spentTokens - !it.readyToServeTimestamp - !it.pacingFrequency - !it.accountId - !it.target - } - } - - def "PBS should return filled line item status report when line item has a scheduled delivery"() { - given: "Line item with a scheduled delivery" - def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(PBSUtils.randomString).tap { - deliverySchedules = [DeliverySchedule.defaultDeliverySchedule] - }]) - generalPlanner.initPlansResponse(plansResponse) - def lineItemId = plansResponse.lineItems[0].lineItemId - def lineItem = plansResponse.lineItems[0] - def deliverySchedule = lineItem.deliverySchedules[0] - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Requesting endpoint" - def lineItemStatusReport = pgPbsService.sendLineItemStatusRequest(lineItemId) - - then: "Line item status report is returned" - def reportTimeZone = lineItemStatusReport.deliverySchedule?.planStartTimeStamp?.zone - assert reportTimeZone - - verifyAll(lineItemStatusReport) { - it.lineItemId == lineItemId - it.deliverySchedule?.planId == deliverySchedule.planId - - it.deliverySchedule.planStartTimeStamp == - timeToReportFormat(deliverySchedule.startTimeStamp, reportTimeZone) - it.deliverySchedule?.planExpirationTimeStamp == - timeToReportFormat(deliverySchedule.endTimeStamp, reportTimeZone) - it.deliverySchedule?.planUpdatedTimeStamp == - timeToReportFormat(deliverySchedule.updatedTimeStamp, reportTimeZone) - - it.deliverySchedule?.tokens?.size() == deliverySchedule.tokens.size() - it.deliverySchedule?.tokens?.first()?.priorityClass == deliverySchedule.tokens[0].priorityClass - it.deliverySchedule?.tokens?.first()?.total == deliverySchedule.tokens[0].total - !it.deliverySchedule?.tokens?.first()?.spent - !it.deliverySchedule?.tokens?.first()?.totalSpent - - it.spentTokens == 0 - it.readyToServeTimestamp.isBefore(ZonedDateTime.now()) - it.pacingFrequency == getDeliveryRateMs(deliverySchedule) - it.accountId == lineItem.accountId - encode(it.target) == encode(lineItem.targeting) - } - } - - def "PBS should return line item status report with an active scheduled delivery"() { - given: "Line item with an active and expired scheduled deliveries" - def inactiveDeliverySchedule = DeliverySchedule.defaultDeliverySchedule.tap { - startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusHours(12) - updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusHours(12) - endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusHours(6) - } - def activeDeliverySchedule = DeliverySchedule.defaultDeliverySchedule.tap { - startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) - updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) - endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).plusHours(12) - } - def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(PBSUtils.randomString).tap { - deliverySchedules = [inactiveDeliverySchedule, activeDeliverySchedule] - }]) - generalPlanner.initPlansResponse(plansResponse) - def lineItemId = plansResponse.lineItems[0].lineItemId - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Requesting endpoint" - def lineItemStatusReport = pgPbsService.sendLineItemStatusRequest(lineItemId) - - then: "Line item status report is returned with an active delivery" - assert lineItemStatusReport.lineItemId == lineItemId - assert lineItemStatusReport.deliverySchedule?.planId == activeDeliverySchedule.planId - } - - def "PBS should return line item status report with increased spent token number when PG auction has happened"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Line item with a scheduled delivery" - def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { - deliverySchedules = [DeliverySchedule.defaultDeliverySchedule.tap { - tokens = [Token.defaultToken] - }] - }]) - generalPlanner.initPlansResponse(plansResponse) - def lineItemId = plansResponse.lineItems[0].lineItemId - def spentTokensNumber = 1 - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "PG bid response is set" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "PBS PG auction is requested" - pgPbsService.sendAuctionRequest(bidRequest) - - when: "Requesting line item status endpoint" - def lineItemStatusReport = pgPbsService.sendLineItemStatusRequest(lineItemId) - - then: "Spent token number in line item status report is increased" - assert lineItemStatusReport.lineItemId == lineItemId - assert lineItemStatusReport.spentTokens == spentTokensNumber - assert lineItemStatusReport.deliverySchedule?.tokens?.first()?.spent == spentTokensNumber - } - - private ZonedDateTime timeToReportFormat(ZonedDateTime givenTime, ZoneId reportTimeZone) { - givenTime.truncatedTo(ChronoUnit.MILLIS).withZoneSameInstant(reportTimeZone) - } - - private Integer getDeliveryRateMs(DeliverySchedule deliverySchedule) { - deliverySchedule.tokens[0].total > 0 - ? (deliverySchedule.endTimeStamp.toInstant().toEpochMilli() - - deliverySchedule.startTimeStamp.toInstant().toEpochMilli()) / - deliverySchedule.tokens[0].total - : null - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgAuctionSpec.groovy deleted file mode 100644 index 4d7ffec88af..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/PgAuctionSpec.groovy +++ /dev/null @@ -1,520 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.UidsCookie -import org.prebid.server.functional.model.bidder.Generic -import org.prebid.server.functional.model.deals.lineitem.FrequencyCap -import org.prebid.server.functional.model.deals.lineitem.LineItem -import org.prebid.server.functional.model.deals.lineitem.LineItemSize -import org.prebid.server.functional.model.deals.lineitem.Price -import org.prebid.server.functional.model.deals.lineitem.RelativePriority -import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting -import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.BidRequestExt -import org.prebid.server.functional.model.request.auction.Bidder -import org.prebid.server.functional.model.request.auction.Imp -import org.prebid.server.functional.model.request.auction.Prebid -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static java.time.ZoneOffset.UTC -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.deals.lineitem.LineItemStatus.DELETED -import static org.prebid.server.functional.model.deals.lineitem.LineItemStatus.PAUSED -import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.HIGH -import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.LOW -import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.MEDIUM -import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.VERY_HIGH -import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.VERY_LOW -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.IN -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.INTERSECTS -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_MEDIA_TYPE -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_SIZE -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.DEVICE_REGION -import static org.prebid.server.functional.model.response.auction.MediaType.BANNER -import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX - -class PgAuctionSpec extends BasePgSpec { - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should return base response after PG auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Auction response contains values according to the payload" - verifyAll(auctionResponse) { - auctionResponse.id == bidRequest.id - auctionResponse.cur == pgConfig.currency - !auctionResponse.bidid - !auctionResponse.customdata - !auctionResponse.nbr - } - - and: "Seat bid corresponds to the request seat bid" - assert auctionResponse.seatbid?.size() == bidRequest.imp.size() - def seatBid = auctionResponse.seatbid[0] - assert seatBid.seat == GENERIC - - assert seatBid.bid?.size() == 1 - - verifyAll(seatBid.bid[0]) { bid -> - (bid.id =~ UUID_REGEX).matches() - bid.impid == bidRequest.imp[0].id - bid.price == bidResponse.seatbid[0].bid[0].price - bid.crid == bidResponse.seatbid[0].bid[0].crid - bid.ext?.prebid?.type == BANNER - bid.ext?.origbidcpm == bidResponse.seatbid[0].bid[0].price - } - } - - def "PBS shouldn't process line item with #reason"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock non matched line item" - generalPlanner.initPlansResponse(plansResponse.tap { - it.lineItems[0].accountId = bidRequest.site.publisher.id - }) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start processing PG deals" - assert !auctionResponse.ext?.debug?.pgmetrics - - where: - reason | plansResponse - - "non matched targeting" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].targeting = new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) - .addTargeting(DEVICE_REGION, IN, [14]) - .build() - } - - "empty targeting" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].targeting = null - } - - "non matched bidder" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].source = PBSUtils.randomString - } - - "inactive status" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].status = DELETED - } - - "paused status" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].status = PAUSED - } - - "expired lifetime" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { - lineItems[0].startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusMinutes(2) - lineItems[0].endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusMinutes(1) - lineItems[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusMinutes(2) - } - } - - def "PBS shouldn't process line item with non matched publisher account id"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock non matched publisher account id line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(PBSUtils.randomNumber as String) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start processing PG deals" - assert !auctionResponse.ext?.debug?.pgmetrics - } - - def "PBS shouldn't start processing PG deals when there is no any line item"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock no line items" - generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start processing PG deals" - assert !auctionResponse.ext?.debug?.pgmetrics - } - - def "PBS shouldn't allow line item with #reason delivery plan take part in auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line item with expired delivery schedule" - def plansResponse = plansResponseClosure(bidRequest) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't allow line item take part in auction" - assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == - plansResponse.lineItems.collect { it.lineItemId } as Set - - where: - reason | plansResponseClosure - - "expired" | { BidRequest bidReq -> - PlansResponse.getDefaultPlansResponse(bidReq.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(2) - lineItems[0].deliverySchedules[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(2) - lineItems[0].deliverySchedules[0].endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(1) - } - } - - "not set" | { BidRequest bidReq -> - PlansResponse.getDefaultPlansResponse(bidReq.site.publisher.id).tap { - lineItems[0].deliverySchedules = [] - } - } - } - - def "PBS should process only first #maxDealsPerBidder line items among the matched ones"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Publisher account id" - def accountId = bidRequest.site.publisher.id - - and: "Planner Mock line items to return #maxDealsPerBidder + 1 line items" - def maxLineItemsToProcess = pgConfig.maxDealsPerBidder - def plansResponse = new PlansResponse(lineItems: (1..maxLineItemsToProcess + 1).collect { - LineItem.getDefaultLineItem(accountId) - }) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "There are #maxLineItemsToProcess + 1 line items are matched and ready to serve" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == maxLineItemsToProcess + 1 - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == maxLineItemsToProcess + 1 - - and: "Only #maxLineItemsToProcess were sent to the bidder" - assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value)?.size() == maxLineItemsToProcess - } - - def "PBS should send to bidder only the first line item among line items with identical deal ids"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Deal id" - def dealId = PBSUtils.randomString - - and: "Planner Mock line items with identical deal ids" - def plansResponse = new PlansResponse(lineItems: (1..2).collect { - LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { it.dealId = dealId } - }) - generalPlanner.initPlansResponse(plansResponse) - def lineItemsNumber = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "There are 2 matched and ready to serve line items" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemsNumber - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItemsNumber - - and: "Only 1 line item was sent to the bidder" - assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value)?.size() == lineItemsNumber - 1 - } - - def "PBS should allow line item with matched to the request bidder alias take part in auction"() { - given: "Bid request with set bidder alias" - def lineItemSource = PBSUtils.randomString - def bidRequest = BidRequest.defaultBidRequest.tap { - def prebid = new Prebid(aliases: [(lineItemSource): GENERIC], debug: 1) - ext = new BidRequestExt(prebid: prebid) - } - - and: "Planner Mock line items with changed line item source" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].source = lineItemSource - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemCount = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Line item was matched by alias bidder and took part in auction" - def pgMetrics = auctionResponse.ext?.debug?.pgmetrics - assert pgMetrics - assert pgMetrics.sentToBidder?.get(lineItemSource)?.size() == lineItemCount - assert pgMetrics.readyToServe?.size() == lineItemCount - assert pgMetrics.matchedWholeTargeting?.size() == lineItemCount - } - - def "PBS should abandon line items with matched user frequency capped ids take part in auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items with added frequency cap" - def fcapId = PBSUtils.randomNumber as String - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].frequencyCaps = [FrequencyCap.defaultFrequencyCap.tap { it.fcapId = fcapId }] - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "User Service Response is set to return frequency capped id identical to the line item fcapId" - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse.tap { - user.ext.fcapIds = [fcapId] - }) - - and: "Cookies header" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS hasn't started processing PG deals as line item was recognized as frequency capped" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedTargetingFcapped?.size() == plansResponse.lineItems.size() - - cleanup: - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) - } - - def "PBS should allow line items with unmatched user frequency capped ids take part in auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items with added frequency cap" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].frequencyCaps = [FrequencyCap.defaultFrequencyCap.tap { fcapId = PBSUtils.randomNumber as String }] - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "User Service Response is set to return frequency capped id not identical to the line item fcapId" - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse.tap { - user.ext.fcapIds = [PBSUtils.randomNumber as String] - }) - - and: "Cookies header" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS hasn't started processing PG deals as line item was recognized as frequency capped" - assert !auctionResponse.ext?.debug?.pgmetrics?.matchedTargetingFcapped - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe - - cleanup: - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) - } - - def "PBS shouldn't use already matched line items by the same bidder during one auction"() { - given: "Bid request with two impressions" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.prebid.bidder = new Bidder(generic: new Generic()) - imp << Imp.defaultImpression - imp[1].ext.prebid.bidder = new Bidder(generic: new Generic()) - } - def accountId = bidRequest.site.publisher.id - - and: "Planner Mock with two line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(accountId).tap { - lineItems << LineItem.getDefaultLineItem(accountId) - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemCount = plansResponse.lineItems.size() - def lineItemIds = plansResponse.lineItems.collect { it.lineItemId } as Set - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is requested" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Two line items are ready to be served" - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItemCount - - and: "Two (as the number of imps) different line items were sent to the bidder" - def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == lineItemCount - assert sentToBidder.sort() == lineItemIds.sort() - - def sentToBidderAsTopMatch = auctionResponse.ext?.debug?.pgmetrics?.sentToBidderAsTopMatch?.get(GENERIC.value) - assert sentToBidderAsTopMatch?.size() == lineItemCount - assert sentToBidderAsTopMatch.sort() == lineItemIds.sort() - } - - def "PBS should send line items with the highest priority to the bidder during auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Planner Mock line items with different priorities" - def lowerPriorityLineItems = [LineItem.getDefaultLineItem(accountId).tap { relativePriority = VERY_LOW }, - LineItem.getDefaultLineItem(accountId).tap { relativePriority = LOW }] - def higherPriorityLineItems = [LineItem.getDefaultLineItem(accountId).tap { relativePriority = MEDIUM }, - LineItem.getDefaultLineItem(accountId).tap { relativePriority = HIGH }, - LineItem.getDefaultLineItem(accountId).tap { relativePriority = VERY_HIGH }] - def lineItems = lowerPriorityLineItems + higherPriorityLineItems - def plansResponse = new PlansResponse(lineItems: lineItems) - def higherPriorityLineItemIds = higherPriorityLineItems.collect { it.lineItemId } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is requested" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "All line items are ready to be served" - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItems.size() - - and: "#maxDealsPerBidder[3] line items were send to bidder" - def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == pgConfig.maxDealsPerBidder - - and: "Those line items with the highest priority were sent" - assert sentToBidder.sort() == higherPriorityLineItemIds.sort() - } - - def "PBS should send line items with the highest CPM to the bidder during auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Planner Mock line items with different CPMs" - def currency = Price.defaultPrice.currency - def lowerCpmLineItems = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: currency) }, - LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 2, currency: currency) }] - def higherCpmLineItems = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 3, currency: currency) }, - LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 4, currency: currency) }, - LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 5, currency: currency) }] - def lineItems = lowerCpmLineItems + higherCpmLineItems - def plansResponse = new PlansResponse(lineItems: lineItems) - def higherCpmLineItemIds = higherCpmLineItems.collect { it.lineItemId } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is requested" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "All line items are ready to be served" - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItems.size() - - and: "#maxDealsPerBidder[3] line items were send to bidder" - def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == pgConfig.maxDealsPerBidder - - and: "Those line items with the highest CPM were sent" - assert sentToBidder.sort() == higherCpmLineItemIds.sort() - } - - def "PBS should send line items with the highest priority to the bidder during auction despite the price"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Planner Mock line items with different priorities and CPMs" - def lineItems = RelativePriority.values().collect { relativePriority -> - LineItem.getDefaultLineItem(accountId).tap { - it.relativePriority = relativePriority - it.price = Price.defaultPrice - } - } - - def plansResponse = new PlansResponse(lineItems: lineItems) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "All line items are ready to be served" - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItems.size() - - and: "#maxDealsPerBidder[3] line items were send to bidder" - def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - def dealsPerBidder = pgConfig.maxDealsPerBidder - assert sentToBidder?.size() == dealsPerBidder - - and: "Those line items with the highest priority were sent" - def prioritizedLineItems = lineItems.sort { it.relativePriority.value } - .take(dealsPerBidder) - assert sentToBidder.sort() == prioritizedLineItems.collect { it.lineItemId }.sort() - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidResponseSpec.groovy deleted file mode 100644 index 2978fb91e0f..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidResponseSpec.groovy +++ /dev/null @@ -1,211 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.lineitem.LineItemSize -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.Format -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.PBSUtils - -import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC - -class PgBidResponseSpec extends BasePgSpec { - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should allow valid bidder response with deals info continue taking part in auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - def lineItemCount = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Set bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder returned valid response with deals info during auction" - assert auctionResponse.ext?.debug?.pgmetrics?.sentToClient?.size() == lineItemCount - assert auctionResponse.ext?.debug?.pgmetrics?.sentToClientAsTopMatch?.size() == lineItemCount - } - - def "PBS should invalidate bidder response when bid id doesn't match to the bid request bid id"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Set bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { - seatbid[0].bid[0].impid = PBSUtils.randomNumber as String - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder response is invalid" - def bidderErrors = auctionResponse.ext?.errors?.get(GENERIC) - def bidId = bidResponse.seatbid[0].bid[0].id - - assert bidderErrors?.size() == 1 - assert bidderErrors[0].message ==~ - /BidId `$bidId` validation messages:.* Error: Bid \"$bidId\" has no corresponding imp in request.*/ - } - - def "PBS should invalidate bidder response when deal id doesn't match to the bid request deal id"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Set bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { - seatbid[0].bid[0].dealid = PBSUtils.randomNumber as String - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder response is invalid" - def bidderErrors = auctionResponse.ext?.errors?.get(GENERIC) - def bidId = bidResponse.seatbid[0].bid[0].id - - assert bidderErrors?.size() == 1 - assert bidderErrors[0].message ==~ /BidId `$bidId` validation messages:.* Warning: / + - /WARNING: Bid "$bidId" has 'dealid' not present in corresponding imp in request.*/ - } - - def "PBS should invalidate bidder response when non-matched to the bid request size is returned"() { - given: "Bid request with set sizes" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].banner.format = [Format.defaultFormat] - } - def impFormat = bidRequest.imp[0].banner.format[0] - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Set bid response with unmatched to the bid request size" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { - seatbid[0].bid[0].w = PBSUtils.randomNumber - } - def bid = bidResponse.seatbid[0].bid[0] - bidder.setResponse(bidRequest.id, bidResponse) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder response is invalid" - assert auctionResponse.ext?.debug?.pgmetrics?.responseInvalidated?.size() == plansResponse.lineItems.size() - - and: "PBS invalidated response as unmatched by size" - def bidderErrors = auctionResponse.ext?.errors?.get(GENERIC) - - assert bidderErrors?.size() == 1 - assert bidderErrors[0].message ==~ /BidId `$bid.id` validation messages:.* Error: / + - /Bid "$bid.id" has 'w' and 'h' not supported by corresponding imp in request\. / + - /Bid dimensions: '${bid.w}x$bid.h', formats in imp: '${impFormat.w}x$impFormat.h'.*/ - } - - def "PBS should invalidate bidder response when non-matched to the PBS line item size response is returned"() { - given: "Bid request" - def newFormat = new Format(w: PBSUtils.randomNumber, h: PBSUtils.randomNumber) - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].banner.format = [Format.defaultFormat, newFormat] - } - - and: "Planner Mock line items with a default size" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].sizes = [LineItemSize.defaultLineItemSize] - } - def lineItemSize = plansResponse.lineItems[0].sizes[0] - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Set bid response with non-matched to the line item size" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { - seatbid[0].bid[0].w = newFormat.w - seatbid[0].bid[0].h = newFormat.h - } - def bid = bidResponse.seatbid[0].bid[0] - bidder.setResponse(bidRequest.id, bidResponse) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder response is invalid" - assert auctionResponse.ext?.debug?.pgmetrics?.responseInvalidated?.size() == plansResponse.lineItems.size() - - and: "PBS invalidated response as not matched by size" - def bidderErrors = auctionResponse.ext?.errors?.get(GENERIC) - - assert bidderErrors?.size() == 1 - assert bidderErrors[0].message ==~ /BidId `$bid.id` validation messages:.* Error: / + - /Bid "$bid.id" has 'w' and 'h' not matched to Line Item\. / + - /Bid dimensions: '${bid.w}x$bid.h', Line Item sizes: '${lineItemSize.w}x$lineItemSize.h'.*/ - } - - def "PBS should invalidate bidder response when line items sizes empty"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].banner.format = [Format.defaultFormat] - } - - and: "Planner Mock line items with a empty sizes" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].sizes = [] - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Set bid response without line items sizes 'h' and 'w' " - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - def bid = bidResponse.seatbid[0].bid[0] - bidder.setResponse(bidRequest.id, bidResponse) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder response is invalid" - assert auctionResponse.ext?.debug?.pgmetrics?.responseInvalidated?.size() == plansResponse.lineItems.size() - - and: "PBS invalidated response as line items sizes empty" - def bidderErrors = auctionResponse.ext?.errors?.get(GENERIC) - - assert bidderErrors?.size() == 1 - assert bidderErrors[0].message == "BidId `${bid.id}` validation messages: " + - "Error: Line item sizes were not found for " + - "bidId ${bid.id} and dealId ${plansResponse.getLineItems()[0].dealId}" - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidderRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidderRequestSpec.groovy deleted file mode 100644 index 0a0219d34ee..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidderRequestSpec.groovy +++ /dev/null @@ -1,167 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.UidsCookie -import org.prebid.server.functional.model.bidder.Generic -import org.prebid.server.functional.model.deals.lineitem.LineItem -import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.Bidder -import org.prebid.server.functional.model.request.auction.Device -import org.prebid.server.functional.model.request.auction.Imp -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Ignore - -class PgBidderRequestSpec extends BasePgSpec { - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should be able to add given device info to the bidder request"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - device = new Device(ua: PBSUtils.randomString, - make: PBSUtils.randomString, - model: PBSUtils.randomString) - } - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "User Service response is set" - def userResponse = UserDetailsResponse.defaultUserResponse - userData.setUserDataResponse(userResponse) - - and: "Cookies with user ids" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS sent a request to the bidder with added device info" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { bidderRequest -> - bidderRequest.user?.ext?.fcapids == userResponse.user.ext.fcapIds - bidderRequest.user.data?.size() == userResponse.user.data.size() - bidderRequest.user.data[0].id == userResponse.user.data[0].name - bidderRequest.user.data[0].segment?.size() == userResponse.user.data[0].segment.size() - bidderRequest.user.data[0].segment[0].id == userResponse.user.data[0].segment[0].id - } - } - - def "PBS should be able to add pmp deals part to the bidder request when PG is enabled"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def accountId = bidRequest.site.publisher.id - def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(accountId), LineItem.getDefaultLineItem(accountId)]) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS sent a request to the bidder with added deals" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) - - assert bidderRequest.imp?.size() == bidRequest.imp.size() - assert bidderRequest.imp[0].pmp?.deals?.size() == plansResponse.lineItems.size() - assert bidderRequest.imp[0].pmp?.deals - assert plansResponse.lineItems.each { lineItem -> - def deal = bidderRequest.imp[0]?.pmp?.deals?.find { it.id == lineItem.dealId } - - assert deal - verifyAll(deal) { - deal?.ext?.line?.lineItemId == lineItem.lineItemId - deal?.ext?.line?.extLineItemId == lineItem.extLineItemId - deal?.ext?.line?.sizes?.size() == lineItem.sizes.size() - deal?.ext?.line?.sizes[0].w == lineItem.sizes[0].w - deal?.ext?.line?.sizes[0].h == lineItem.sizes[0].h - } - } - } - - @Ignore - def "PBS shouldn't add already top matched line item by first impression to the second impression deals bidder request section"() { - given: "Bid request with two impressions" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.prebid.bidder = new Bidder(generic: new Generic()) - imp << Imp.defaultImpression - imp[1].ext.prebid.bidder = new Bidder(generic: new Generic()) - } - - and: "Planner Mock line items" - def accountId = bidRequest.site.publisher.id - def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(accountId), LineItem.getDefaultLineItem(accountId)]) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS sent a request to the bidder with two impressions" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert bidderRequest.imp?.size() == bidRequest.imp.size() - - and: "First impression contains 2 deals according to the line items" - def firstRequestImp = bidderRequest.imp.find { it.id == bidRequest.imp[0].id } - assert firstRequestImp?.pmp?.deals?.size() == plansResponse.lineItems.size() - assert plansResponse.lineItems.each { lineItem -> - def deal = firstRequestImp.pmp.deals.find { it.id == lineItem.dealId } - - assert deal - verifyAll(deal) { - deal?.ext?.line?.lineItemId == lineItem.lineItemId - deal?.ext?.line?.extLineItemId == lineItem.extLineItemId - deal?.ext?.line?.sizes?.size() == lineItem.sizes.size() - deal?.ext?.line?.sizes[0].w == lineItem.sizes[0].w - deal?.ext?.line?.sizes[0].h == lineItem.sizes[0].h - } - } - - def topMatchLineItemId = firstRequestImp.pmp.deals.first().ext.line.lineItemId - def secondRequestImp = bidderRequest.imp.find { it.id == bidRequest.imp[1].id } - - and: "Second impression contains only 1 deal excluding already top matched line items by the first impression" - assert secondRequestImp.pmp.deals.size() == plansResponse.lineItems.size() - 1 - assert !(secondRequestImp.pmp.deals.collect { it.ext.line.lineItemId } in topMatchLineItemId) - - assert plansResponse.lineItems.findAll { it.lineItemId != topMatchLineItemId }.each { lineItem -> - def deal = secondRequestImp.pmp.deals.find { it.id == lineItem.dealId } - - assert deal - verifyAll(deal) { - deal?.ext?.line?.lineItemId == lineItem.lineItemId - deal?.ext?.line?.extLineItemId == lineItem.extLineItemId - deal?.ext?.line?.sizes?.size() == lineItem.sizes.size() - deal?.ext?.line?.sizes[0].w == lineItem.sizes[0].w - deal?.ext?.line?.sizes[0].h == lineItem.sizes[0].h - } - } - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgDealsOnlySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgDealsOnlySpec.groovy deleted file mode 100644 index d5677d963cf..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/PgDealsOnlySpec.groovy +++ /dev/null @@ -1,190 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.bidder.BidderName -import org.prebid.server.functional.model.bidder.Generic -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.BidRequestExt -import org.prebid.server.functional.model.request.auction.Prebid -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.PBSUtils - -import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC -import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID - -class PgDealsOnlySpec extends BasePgSpec { - - def "PBS shouldn't call bidder when bidder pgdealsonly flag is set to true and no available PG line items"() { - given: "Bid request with set pgdealsonly flag" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(pgDealsOnly: true) - def initialBidderRequestCount = bidder.requestCount - - and: "No line items response" - generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't called bidder during auction" - assert initialBidderRequestCount == bidder.requestCount - - and: "PBS returns a not calling bidder warning" - def auctionWarnings = auctionResponse.ext?.warnings?.get(PREBID) - assert auctionWarnings.size() == 1 - assert auctionWarnings[0].code == 999 - assert auctionWarnings[0].message == - "Not calling $GENERIC.value bidder for impressions ${bidRequest.imp[0].id}" + - " due to pgdealsonly flag and no available PG line items." - } - - def "PBS shouldn't call bidder when bidder alias is set, bidder pgdealsonly flag is set to true and no available PG line items"() { - given: "Bid request with set bidder alias and pgdealsonly flag" - def bidderAliasName = PBSUtils.randomString - def bidRequest = BidRequest.defaultBidRequest.tap { - def prebid = new Prebid(aliases: [(bidderAliasName): BidderName.GENERIC], debug: 1) - ext = new BidRequestExt(prebid: prebid) - } - bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(pgDealsOnly: true) - def initialBidderRequestCount = bidder.requestCount - - and: "No line items response" - generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't called bidder during auction" - assert initialBidderRequestCount == bidder.requestCount - - and: "PBS returns a not calling bidder warning" - def auctionWarnings = auctionResponse.ext?.warnings?.get(PREBID) - assert auctionWarnings.size() == 1 - assert auctionWarnings[0].code == 999 - assert auctionWarnings[0].message == - "Not calling $GENERIC.value bidder for impressions ${bidRequest.imp[0].id}" + - " due to pgdealsonly flag and no available PG line items." - } - - def "PBS should call bidder when bidder pgdealsonly flag is set to false and no available PG line items"() { - given: "Bid request with set pgdealsonly flag" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(pgDealsOnly: false) - def initialBidderRequestCount = bidder.requestCount - - and: "No line items response" - generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS has called bidder during auction" - assert initialBidderRequestCount + 1 == bidder.requestCount - assert !auctionResponse.ext?.warnings - } - - def "PBS should return an error when bidder dealsonly flag is set to true, no available PG line items and bid response misses 'dealid' field"() { - given: "Bid request with set dealsonly flag" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(dealsOnly: true) - def initialBidderRequestCount = bidder.requestCount - - and: "No line items response" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) - - and: "Bid response with missing 'dealid' field" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidResponse.seatbid[0].bid[0].dealid = null - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder was requested" - assert initialBidderRequestCount + 1 == bidder.requestCount - - and: "PBS returns an error of missing 'dealid' field in bid" - def bidErrors = auctionResponse.ext?.errors?.get(GENERIC) - def bidId = bidResponse.seatbid[0].bid[0].id - - assert bidErrors?.size() == 1 - assert bidErrors[0].code == 5 - assert bidErrors[0].message ==~ /BidId `$bidId` validation messages:.* Error: / + - /Bid "$bidId" missing required field 'dealid'.*/ - } - - def "PBS should add dealsonly flag when it is not specified and pgdealsonly flag is set to true"() { - given: "Bid request with set pgdealsonly flag, dealsonly is not specified" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(pgDealsOnly: true) - def initialBidderRequestCount = bidder.requestCount - - and: "No line items response" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) - - and: "Bid response with missing 'dealid' field to check dealsonly flag was added and worked" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidResponse.seatbid[0].bid[0].dealid = null - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder was requested" - assert initialBidderRequestCount + 1 == bidder.requestCount - - and: "PBS added dealsonly flag to the bidder request" - assert auctionResponse.ext?.debug?.resolvedRequest?.imp?.first()?.ext?.prebid?.bidder?.generic?.dealsOnly - - and: "PBS returns an error of missing 'dealid' field in bid" - def bidErrors = auctionResponse.ext?.errors?.get(GENERIC) - def bidId = bidResponse.seatbid[0].bid[0].id - - assert bidErrors?.size() == 1 - assert bidErrors[0].code == 5 - assert bidErrors[0].message ==~ /BidId `$bidId` validation messages:.* Error: / + - /Bid "$bidId" missing required field 'dealid'.*/ - } - - def "PBS shouldn't return an error when bidder dealsonly flag is set to true, no available PG line items and bid response misses 'dealid' field"() { - given: "Bid request with set dealsonly flag" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(dealsOnly: false) - def initialBidderRequestCount = bidder.requestCount - - and: "No line items response" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) - - and: "Bid response with missing 'dealid' field" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidResponse.seatbid[0].bid[0].dealid = null - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder was requested" - assert initialBidderRequestCount + 1 == bidder.requestCount - - and: "PBS hasn't returned an error" - !auctionResponse.ext?.errors - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PlansSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PlansSpec.groovy deleted file mode 100644 index 6d277034751..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/PlansSpec.groovy +++ /dev/null @@ -1,57 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils - -import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 -import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME -import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER -import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER -import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX - -class PlansSpec extends BasePgSpec { - - def "PBS should be able to send a request to General Planner"() { - given: "General Planner response is set" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString), OK_200) - - when: "PBS sends request to General Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - - then: "Request is sent" - PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == 1 } - } - - def "PBS should retry request to General Planner when first request fails"() { - given: "Bad General Planner response" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString), INTERNAL_SERVER_ERROR_500) - - when: "PBS sends request to General Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - - then: "Request is sent two times" - PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == 2 } - } - - def "PBS should send appropriate headers when requests plans from General Planner"() { - when: "PBS sends request to General Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - - then: "Request with headers is sent" - def plansRequestHeaders = generalPlanner.lastRecordedPlansRequestHeaders - assert plansRequestHeaders - - and: "Request has an authorization header with a basic auth token" - def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) - assert plansRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] - - and: "Request has a header with uuid value" - def uuidHeader = plansRequestHeaders.get(PG_TRX_ID_HEADER) - assert uuidHeader?.size() == 1 - assert (uuidHeader[0] =~ UUID_REGEX).matches() - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/RegisterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/RegisterSpec.groovy deleted file mode 100644 index a62a10cd594..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/RegisterSpec.groovy +++ /dev/null @@ -1,229 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static java.time.ZoneOffset.UTC -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME -import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER -import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER -import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX - -class RegisterSpec extends BasePgSpec { - - def setupSpec() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should be able to register its instance in Planner on demand"() { - given: "Properties values from PBS config" - def host = pgConfig.hostId - def vendor = pgConfig.vendor - def region = pgConfig.region - - and: "Initial Planner request count" - def initialRequestCount = generalPlanner.requestCount - - when: "PBS sends request to Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "Request counter is increased" - PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } - - and: "PBS instance is healthy" - def registerRequest = generalPlanner.lastRecordedRegisterRequest - assert registerRequest.healthIndex >= 0 && registerRequest.healthIndex <= 1 - - and: "Host, vendor and region are appropriate to the config" - assert registerRequest.hostInstanceId == host - assert registerRequest.vendor == vendor - assert registerRequest.region == region - - and: "Delivery Statistics Report doesn't have delivery specific data" - verifyAll(registerRequest.status.dealsStatus) { delStatsReport -> - (delStatsReport.reportId =~ UUID_REGEX).matches() - delStatsReport.instanceId == host - delStatsReport.vendor == vendor - delStatsReport.region == region - !delStatsReport.lineItemStatus - !delStatsReport.dataWindowStartTimeStamp - !delStatsReport.dataWindowEndTimeStamp - delStatsReport.reportTimeStamp.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) - } - } - - def "PBS should send a register request with appropriate headers"() { - when: "Initiating PBS to register its instance" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "Request with headers is sent" - def registerRequestHeaders = generalPlanner.lastRecordedRegisterRequestHeaders - assert registerRequestHeaders - - and: "Request has an authorization header with a basic auth token" - def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) - assert registerRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] - - and: "Request has a header with uuid value" - def uuidHeader = registerRequestHeaders.get(PG_TRX_ID_HEADER) - assert uuidHeader?.size() == 1 - assert (uuidHeader[0] =~ UUID_REGEX).matches() - } - - def "PBS should be able to register its instance in Planner providing active PBS line items info"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - def lineItem = plansResponse.lineItems[0] - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial Planner request count" - def initialRequestCount = generalPlanner.requestCount - - when: "PBS sends request to Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "Request counter is increased" - PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } - - and: "Delivery Statistics Report has active line item data" - def registerRequest = generalPlanner.lastRecordedRegisterRequest - def delStatsReport = registerRequest.status?.dealsStatus - assert delStatsReport - def lineItemStatus = delStatsReport.lineItemStatus - - assert lineItemStatus?.size() == plansResponse.lineItems.size() - verifyAll(lineItemStatus) { - lineItemStatus[0].lineItemSource == lineItem.source - lineItemStatus[0].lineItemId == lineItem.lineItemId - lineItemStatus[0].dealId == lineItem.dealId - lineItemStatus[0].extLineItemId == lineItem.extLineItemId - } - - and: "Line item wasn't used in auction" - verifyAll(lineItemStatus) { - !lineItemStatus[0].accountAuctions - !lineItemStatus[0].targetMatched - !lineItemStatus[0].sentToBidder - !lineItemStatus[0].spentTokens - } - - cleanup: - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should be able to register its instance in Planner providing line items status after auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - def lineItem = plansResponse.lineItems[0] - def lineItemCount = plansResponse.lineItems.size() as Long - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial Planner request count" - def initialRequestCount = generalPlanner.requestCount - - and: "Auction is requested" - pgPbsService.sendAuctionRequest(bidRequest) - - when: "PBS sends request to Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "Request counter is increased" - PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } - - and: "Delivery Statistics Report has info about auction" - def registerRequest = generalPlanner.lastRecordedRegisterRequest - def delStatsReport = registerRequest.status.dealsStatus - assert delStatsReport - - and: "Delivery Statistics Report has correct line item status data" - def lineItemStatus = delStatsReport.lineItemStatus - - assert lineItemStatus?.size() as Long == lineItemCount - verifyAll(lineItemStatus) { - lineItemStatus[0].lineItemSource == lineItem.source - lineItemStatus[0].lineItemId == lineItem.lineItemId - lineItemStatus[0].dealId == lineItem.dealId - lineItemStatus[0].extLineItemId == lineItem.extLineItemId - } - - and: "Line item was used in auction" - verifyAll(lineItemStatus) { - lineItemStatus[0].accountAuctions == lineItemCount - lineItemStatus[0].targetMatched == lineItemCount - lineItemStatus[0].sentToBidder == lineItemCount - lineItemStatus[0].spentTokens == lineItemCount - } - - cleanup: - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should update auction count when register its instance in Planner after auction"() { - given: "Initial auction count" - def initialRequestCount = generalPlanner.requestCount - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } - def initialAuctionCount = generalPlanner.lastRecordedRegisterRequest?.status?.dealsStatus?.clientAuctions - - and: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial Planner request count" - initialRequestCount = generalPlanner.requestCount - - and: "Auction is requested" - pgPbsService.sendAuctionRequest(bidRequest) - - when: "PBS sends request to Planner" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) - - then: "Request counter is increased" - PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } - - and: "Delivery Statistics Report has info about auction" - def registerRequest = generalPlanner.lastRecordedRegisterRequest - assert registerRequest.status?.dealsStatus?.clientAuctions == initialAuctionCount + 1 - - cleanup: - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/ReportSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/ReportSpec.groovy deleted file mode 100644 index 75c73d0f534..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/ReportSpec.groovy +++ /dev/null @@ -1,559 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.lineitem.DeliverySchedule -import org.prebid.server.functional.model.deals.lineitem.LineItem -import org.prebid.server.functional.model.deals.lineitem.Token -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils - -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -import static java.time.ZoneOffset.UTC -import static org.mockserver.model.HttpStatusCode.CONFLICT_409 -import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 -import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.prebid.server.functional.model.deals.lineitem.LineItem.TIME_PATTERN -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD -import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME -import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER -import static org.prebid.server.functional.util.HttpUtil.CHARSET_HEADER_VALUE -import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER -import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER_VALUE -import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER -import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX - -class ReportSpec extends BasePgSpec { - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS shouldn't send delivery statistics when PBS doesn't have reports to send"() { - given: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - when: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "Delivery Statistics Service request count is not changed" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount } - } - - def "PBS shouldn't send delivery statistics when delivery report batch is created but doesn't have reports to send"() { - given: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - when: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "Delivery Statistics Service request count is not changed" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount } - } - - def "PBS should send a report request with appropriate headers"() { - given: "Initial report sent request count is taken" - def initialRequestCount = deliveryStatistics.requestCount - - and: "Line items are fetched" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) - updateLineItemsAndWait() - - and: "Delivery report batch is created" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - when: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - and: "PBS sends a report request to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - then: "Request headers corresponds to the payload" - def deliveryRequestHeaders = deliveryStatistics.lastRecordedDeliveryRequestHeaders - assert deliveryRequestHeaders - - and: "Request has an authorization header with a basic auth token" - def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) - assert deliveryRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] - - and: "Request has a header with uuid value" - def uuidHeader = deliveryRequestHeaders.get(PG_TRX_ID_HEADER) - assert uuidHeader?.size() == 1 - assert (uuidHeader[0] =~ UUID_REGEX).matches() - - and: "Request has a content type header" - assert deliveryRequestHeaders.get(CONTENT_TYPE_HEADER) == ["$CONTENT_TYPE_HEADER_VALUE;$CHARSET_HEADER_VALUE"] - } - - def "PBS should send delivery statistics report when delivery progress report with one line item is created"() { - given: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - and: "Time before report is sent" - def startTime = ZonedDateTime.now(UTC) - - and: "Set Planner response to return one line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(PBSUtils.randomString) - def lineItem = plansResponse.lineItems[0] - generalPlanner.initPlansResponse(plansResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - when: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report request to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - and: "Report request should correspond to the payload" - def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest - def endTime = ZonedDateTime.now(ZoneId.from(UTC)) - - verifyAll(reportRequest) { - (reportRequest.reportId =~ UUID_REGEX).matches() - reportRequest.instanceId == pgConfig.hostId - reportRequest.vendor == pgConfig.vendor - reportRequest.region == pgConfig.region - !reportRequest.clientAuctions - - reportRequest.reportTimeStamp.isBefore(endTime) - reportRequest.dataWindowStartTimeStamp.isBefore(startTime) - reportRequest.dataWindowEndTimeStamp.isAfter(startTime) - reportRequest.dataWindowEndTimeStamp.isBefore(endTime) - reportRequest.reportTimeStamp.isAfter(reportRequest.dataWindowEndTimeStamp) - } - - and: "Report line items should have an appropriate to the initially set line items info" - assert reportRequest.lineItemStatus?.size() == 1 - def lineItemStatus = reportRequest.lineItemStatus[0] - - verifyAll(lineItemStatus) { - lineItemStatus.lineItemSource == lineItem.source - lineItemStatus.lineItemId == lineItem.lineItemId - lineItemStatus.dealId == lineItem.dealId - lineItemStatus.extLineItemId == lineItem.extLineItemId - !lineItemStatus.accountAuctions - !lineItemStatus.domainMatched - !lineItemStatus.targetMatched - !lineItemStatus.targetMatchedButFcapped - !lineItemStatus.targetMatchedButFcapLookupFailed - !lineItemStatus.pacingDeferred - !lineItemStatus.sentToBidder - !lineItemStatus.sentToBidderAsTopMatch - !lineItemStatus.receivedFromBidder - !lineItemStatus.receivedFromBidderInvalidated - !lineItemStatus.sentToClient - !lineItemStatus.sentToClientAsTopMatch - !lineItemStatus.lostToLineItems - !lineItemStatus.events - !lineItemStatus.readyAt - !lineItemStatus.spentTokens - !lineItemStatus.pacingFrequency - - lineItemStatus.deliverySchedule?.size() == 1 - } - - def timeFormatter = DateTimeFormatter.ofPattern(TIME_PATTERN) - def deliverySchedule = lineItemStatus.deliverySchedule[0] - - verifyAll(deliverySchedule) { - deliverySchedule.planId == lineItem.deliverySchedules[0].planId - timeFormatter.format(deliverySchedule.planStartTimeStamp) == - timeFormatter.format(lineItem.deliverySchedules[0].startTimeStamp) - timeFormatter.format(deliverySchedule.planUpdatedTimeStamp) == - timeFormatter.format(lineItem.deliverySchedules[0].updatedTimeStamp) - timeFormatter.format(deliverySchedule.planExpirationTimeStamp) == - timeFormatter.format(lineItem.deliverySchedules[0].endTimeStamp) - - deliverySchedule.tokens?.size() == 1 - } - - verifyAll(deliverySchedule.tokens[0]) { tokens -> - tokens.priorityClass == lineItem.deliverySchedules[0].tokens[0].priorityClass - tokens.total == lineItem.deliverySchedules[0].tokens[0].total - tokens.spent == 0 - tokens.totalSpent == 0 - } - } - - def "PBS should send a correct delivery statistics report when auction with one line item is happened"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - and: "Time before report is sent" - def startTime = ZonedDateTime.now(UTC) - - and: "Set Planner response to return one line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - def lineItem = plansResponse.lineItems[0] - def lineItemCount = plansResponse.lineItems.size() as Long - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - when: "Auction request to PBS is sent" - pgPbsService.sendAuctionRequest(bidRequest) - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report request to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - and: "Report request should be sent after the test start" - def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest - assert reportRequest.reportTimeStamp.isAfter(startTime) - - and: "Request should contain correct number of client auctions made" - assert reportRequest.clientAuctions == 1 - - and: "Report line items should have an appropriate to the initially set line item info" - assert reportRequest.lineItemStatus?.size() == 1 - def lineItemStatus = reportRequest.lineItemStatus[0] - - verifyAll(lineItemStatus) { - lineItemStatus.lineItemSource == lineItem.source - lineItemStatus.lineItemId == lineItem.lineItemId - lineItemStatus.dealId == lineItem.dealId - lineItemStatus.extLineItemId == lineItem.extLineItemId - } - - and: "Report should have the right PG metrics info" - verifyAll(lineItemStatus) { - lineItemStatus?.accountAuctions == lineItemCount - lineItemStatus?.targetMatched == lineItemCount - lineItemStatus?.sentToBidder == lineItemCount - lineItemStatus?.sentToBidderAsTopMatch == lineItemCount - } - - and: "Report line item should have a delivery schedule" - assert lineItemStatus.deliverySchedule?.size() == 1 - assert lineItemStatus.deliverySchedule[0].planId == lineItem.deliverySchedules[0].planId - } - - def "PBS should use line item token with the highest priority"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - and: "Time before report is sent" - def startTime = ZonedDateTime.now(UTC) - - and: "Set Planner response to return one line item" - def highestPriorityToken = new Token(priorityClass: 1, total: 2) - def lowerPriorityToken = new Token(priorityClass: 3, total: 2) - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - def tokens = [highestPriorityToken, lowerPriorityToken] - lineItems[0].deliverySchedules[0].tokens = tokens - } - def tokens = plansResponse.lineItems[0].deliverySchedules[0].tokens - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - when: "Auction request to PBS is sent" - pgPbsService.sendAuctionRequest(bidRequest) - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report request to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - and: "Report request should be sent after the test start" - def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest - assert reportRequest.reportTimeStamp.isAfter(startTime) - - and: "Token with the highest priority was used" - def reportTokens = reportRequest.lineItemStatus?.first()?.deliverySchedule?.first()?.tokens - assert reportTokens - assert reportTokens.size() == tokens.size() - def usedToken = reportTokens.find { it.priorityClass == highestPriorityToken.priorityClass } - assert usedToken?.total == highestPriorityToken.total - assert usedToken?.spent == 1 - assert usedToken?.totalSpent == 1 - - and: "Token with a lower priority wasn't used" - def notUsedToken = reportTokens.find { it.priorityClass == lowerPriorityToken.priorityClass } - assert notUsedToken?.total == lowerPriorityToken.total - assert notUsedToken?.spent == 0 - assert notUsedToken?.totalSpent == 0 - } - - def "PBS shouldn't consider line item as used when bidder responds with non-deals specific info"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - and: "Time before report is sent" - def startTime = ZonedDateTime.now(UTC) - - and: "Set Planner response to return one line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Non-deals bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - when: "Auction request to PBS is sent" - pgPbsService.sendAuctionRequest(bidRequest) - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report request to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - and: "Report request should be sent after the test start" - def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest - assert reportRequest.reportTimeStamp.isAfter(startTime) - - and: "Line item token wasn't used" - def reportTokens = reportRequest.lineItemStatus?.first()?.deliverySchedule?.first()?.tokens - assert reportTokens?.size() == plansResponse.lineItems[0].deliverySchedules[0].tokens.size() - assert reportTokens[0].spent == 0 - assert reportTokens[0].totalSpent == 0 - } - - def "PBS should send additional report when line items number exceeds PBS 'line-items-per-report' property"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Already recorded request count is reset" - deliveryStatistics.resetRecordedRequests() - deliveryStatistics.setResponse() - - and: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - and: "Set Planner response to return #lineItemsPerReport + 1 line items" - def lineItemsPerReport = pgConfig.lineItemsPerReport - def plansResponse = new PlansResponse(lineItems: (1..lineItemsPerReport + 1).collect { - LineItem.getDefaultLineItem(accountId) - }) - generalPlanner.initPlansResponse(plansResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - when: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends two report requests to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 2 } - - and: "Two reports are sent" - def reportRequests = deliveryStatistics.recordedDeliveryStatisticsReportRequests - assert reportRequests.size() == 2 - - and: "Two reports were sent with #lineItemsPerReport and 1 number of line items" - assert [reportRequests[-2].lineItemStatus.size(), reportRequests[-1].lineItemStatus.size()].sort() == - [lineItemsPerReport, 1].sort() - } - - def "PBS should save reports for later sending when response from Delivery Statistics was unsuccessful"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Set Planner response to return 1 line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(accountId) - generalPlanner.initPlansResponse(plansResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "Delivery Statistics Service response is set to return a bad status code" - deliveryStatistics.reset() - deliveryStatistics.setResponse(INTERNAL_SERVER_ERROR_500) - - when: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report to Delivery Statistics" - PBSUtils.waitUntil { deliveryStatistics.requestCount == 1 } - - when: "Delivery Statistics Service response is set to return a success response" - deliveryStatistics.reset() - deliveryStatistics.setResponse(OK_200) - - and: "PBS is requested to send a report to Delivery Statistics for the second time" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS for the second time sends the same report to the Delivery Statistics Service" - PBSUtils.waitUntil { deliveryStatistics.requestCount == 1 } - } - - def "PBS shouldn't save reports for later sending when Delivery Statistics response is Conflict 409"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Set Planner response to return 1 line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(accountId) - generalPlanner.initPlansResponse(plansResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "Delivery Statistics Service response is set to return a Conflict status code" - deliveryStatistics.reset() - deliveryStatistics.setResponse(CONFLICT_409) - - and: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - - when: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report to Delivery Statistics" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - and: "PBS is requested to send a report to Delivery Statistics for the second time" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS doesn't request Delivery Statistics Service for the second time" - assert deliveryStatistics.requestCount == initialRequestCount + 1 - } - - def "PBS should change active delivery plan when the current plan lifetime expires"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - def accountId = bidRequest.site.publisher.id - - and: "Initial Delivery Statistics Service request count" - def initialRequestCount = deliveryStatistics.requestCount - def auctionCount = 2 - - and: "Current delivery plan which expires in 2 seconds" - def currentPlanTimeToLive = 2 - def currentDeliverySchedule = new DeliverySchedule(planId: PBSUtils.randomNumber as String, - startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), - updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), - endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(currentPlanTimeToLive), - tokens: [new Token(priorityClass: 1, total: 1000)]) - - and: "Next delivery plan" - def nextDeliverySchedule = new DeliverySchedule(planId: PBSUtils.randomNumber as String, - startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(currentPlanTimeToLive), - updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(currentPlanTimeToLive), - endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusHours(1), - tokens: [new Token(priorityClass: 1, total: 500)]) - - and: "Set Planner response to return line item with two delivery plans" - def plansResponse = PlansResponse.getDefaultPlansResponse(accountId).tap { - lineItems[0].deliverySchedules = [currentDeliverySchedule, nextDeliverySchedule] - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "PBS requests Planner line items" - updateLineItemsAndWait() - - and: "Auction request to PBS is sent for the first time" - pgPbsService.sendAuctionRequest(bidRequest) - - when: "Current delivery plan lifetime is expired" - PBSUtils.waitUntil({ ZonedDateTime.now(ZoneId.from(UTC)).isAfter(currentDeliverySchedule.endTimeStamp) }, - (currentPlanTimeToLive * 1000) + 1000) - - and: "PBS requests Planner line items which also forces current PBS line items to be updated" - generalPlanner.initPlansResponse(plansResponse) - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) - - and: "Auction request to PBS is sent for the second time" - bidder.setResponse(bidRequest.id, bidResponse) - pgPbsService.sendAuctionRequest(bidRequest) - - and: "PBS generates delivery report batch" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) - - and: "PBS is requested to send a report to Delivery Statistics" - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) - - then: "PBS sends a report to Delivery Statistics" - PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } - - and: "Report has info about 2 happened auctions" - def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest - assert reportRequest.clientAuctions == auctionCount - assert reportRequest.lineItemStatus?.size() == plansResponse.lineItems.size() - assert reportRequest.lineItemStatus[0].accountAuctions == auctionCount - - and: "One line item during each auction was sent to the bidder" - assert reportRequest.lineItemStatus[0].sentToBidder == auctionCount - - and: "Report contains two delivery plans info" - def reportDeliverySchedules = reportRequest.lineItemStatus[0].deliverySchedule - assert reportDeliverySchedules?.size() == plansResponse.lineItems[0].deliverySchedules.size() - - and: "One token was used during the first auction by the first delivery plan" - assert reportDeliverySchedules.find { it.planId == currentDeliverySchedule.planId }?.tokens[0].spent == 1 - - and: "One token was used from another delivery plan during the second auction after first delivery plan lifetime expired" - assert reportDeliverySchedules.find { it.planId == nextDeliverySchedule.planId }?.tokens[0].spent == 1 - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingFirstPartyDataSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingFirstPartyDataSpec.groovy deleted file mode 100644 index a1552071a8d..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingFirstPartyDataSpec.groovy +++ /dev/null @@ -1,707 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.AppExt -import org.prebid.server.functional.model.request.auction.AppExtData -import org.prebid.server.functional.model.request.auction.Banner -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.BidRequestExt -import org.prebid.server.functional.model.request.auction.BidderConfig -import org.prebid.server.functional.model.request.auction.BidderConfigOrtb -import org.prebid.server.functional.model.request.auction.DoohExt -import org.prebid.server.functional.model.request.auction.DoohExtData -import org.prebid.server.functional.model.request.auction.ExtPrebidBidderConfig -import org.prebid.server.functional.model.request.auction.Imp -import org.prebid.server.functional.model.request.auction.ImpExtContext -import org.prebid.server.functional.model.request.auction.ImpExtContextData -import org.prebid.server.functional.model.request.auction.Prebid -import org.prebid.server.functional.model.request.auction.Site -import org.prebid.server.functional.model.request.auction.SiteExt -import org.prebid.server.functional.model.request.auction.SiteExtData -import org.prebid.server.functional.model.request.auction.User -import org.prebid.server.functional.model.request.auction.UserExt -import org.prebid.server.functional.model.request.auction.UserExtData -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.service.PrebidServerException -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Shared - -import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.IN -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.INTERSECTS -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.MATCHES -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_BUYER_ID -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_BUYER_IDS -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_KEYWORDS -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_LANGUAGE -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_BUYER_UID -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_BUYER_UIDS -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_KEYWORDS -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_YOB -import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP -import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH -import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE - -class TargetingFirstPartyDataSpec extends BasePgSpec { - - @Shared - String stringTargetingValue = PBSUtils.randomString - @Shared - Integer integerTargetingValue = PBSUtils.randomNumber - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should support both scalar and array String inputs by '#ufpdTargetingType' for INTERSECTS matching function"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - user = User.defaultUser.tap { - ext = new UserExt(data: userExtData) - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(ufpdTargetingType, INTERSECTS, [stringTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - ufpdTargetingType | userExtData - UFPD_BUYER_UID | new UserExtData(buyeruid: stringTargetingValue) - UFPD_KEYWORDS | new UserExtData(keywords: [stringTargetingValue]) - } - - def "PBS should support both scalar and array Integer inputs by '#ufpdTargetingType' for INTERSECTS matching function"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - user = User.defaultUser.tap { - ext = new UserExt(data: userExtData) - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(ufpdTargetingType, INTERSECTS, [stringTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - ufpdTargetingType | userExtData - UFPD_BUYER_UID | new UserExtData(buyeruid: stringTargetingValue) - UFPD_BUYER_UIDS | new UserExtData(buyeruids: [stringTargetingValue]) - } - - def "PBS should support taking Site First Party Data from #place source"() { - given: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.getAccountId()).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, INTERSECTS, [stringTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - place | bidRequest - "imp[].ext.context" | BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - ext.context = new ImpExtContext(data: new ImpExtContextData(language: stringTargetingValue)) - }] - } - "site" | BidRequest.defaultBidRequest.tap { - site = Site.defaultSite.tap { - ext = new SiteExt(data: new SiteExtData(language: stringTargetingValue)) - } - } - "imp[].ext.data" | BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - ext.data = new ImpExtContextData(language: stringTargetingValue) - }] - } - } - - def "PBS should support String array input for Site First Party Data to be matched by INTERSECTS matching function"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - banner = Banner.defaultBanner - ext.context = new ImpExtContext(data: new ImpExtContextData(keywords: [stringTargetingValue])) - }] - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_KEYWORDS, INTERSECTS, [stringTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should support both scalar and array Integer inputs in Site First Party Data ('#targetingType') by INTERSECTS matching function"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - banner = Banner.defaultBanner - ext.context = new ImpExtContext(data: impExtContextData) - }] - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(targetingType, INTERSECTS, [integerTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - targetingType | impExtContextData - SFPD_BUYER_ID | new ImpExtContextData(buyerId: integerTargetingValue) - SFPD_BUYER_IDS | new ImpExtContextData(buyerIds: [integerTargetingValue]) - } - - def "PBS shouldn't throw a NPE for Site First Party Data when its Ext is absent and targeting INTERSECTS matching type is selected"() { - given: "Bid request with set site first party data in bidRequest.site" - def bidRequest = BidRequest.defaultBidRequest.tap { - site = Site.defaultSite.tap { - keywords = stringTargetingValue - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_KEYWORDS, INTERSECTS, [stringTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS successfully processed request" - notThrown(PrebidServerException) - - and: "PBS hasn't had PG auction as request targeting is not specified in the right place" - assert !auctionResponse.ext?.debug?.pgmetrics - } - - def "PBS should support taking User FPD from bidRequest.user by #matchingFunction matching function"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - user = User.defaultUser.tap { - updateUserFieldGeneric(it) - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(ufpdTargetingType, matchingFunction, [targetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - ufpdTargetingType | updateUserFieldGeneric | targetingValue | matchingFunction - UFPD_BUYER_UID | { it.buyeruid = stringTargetingValue } | stringTargetingValue | INTERSECTS - UFPD_BUYER_UID | { it.buyeruid = stringTargetingValue } | stringTargetingValue | IN - UFPD_YOB | { it.yob = integerTargetingValue } | integerTargetingValue | INTERSECTS - UFPD_YOB | { it.yob = integerTargetingValue } | integerTargetingValue | IN - } - - def "PBS should support taking User FPD from bidRequest.user by MATCHES matching function"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - user = User.defaultUser.tap { - it.buyeruid = stringTargetingValue - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should be able to match site FPD targeting taken from different sources by INTERSECTS matching function"() { - given: "Bid request with set site FPD in different request places" - def bidRequest = getSiteFpdBidRequest(siteLanguage, appLanguage, doohLanguage, impLanguage) - - and: "Planner response with INTERSECTS 1 of site FPD values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.getAccountId()).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, INTERSECTS, [stringTargetingValue, PBSUtils.randomString]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - siteLanguage | appLanguage | doohLanguage | impLanguage - stringTargetingValue | null | null | PBSUtils.randomString - null | stringTargetingValue | null | PBSUtils.randomString - null | null | stringTargetingValue | stringTargetingValue - } - - def "PBS should be able to match site FPD targeting taken from different sources by MATCHES matching function"() { - given: "Bid request with set site FPD in different request places" - def bidRequest = getSiteFpdBidRequest(siteLanguage, appLanguage, doohLanguage, impLanguage) - - and: "Planner response with MATCHES 1 of site FPD values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.getAccountId()).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - siteLanguage | appLanguage | doohLanguage | impLanguage - stringTargetingValue | null | null | PBSUtils.randomString - null | stringTargetingValue | null | PBSUtils.randomString - null | null | stringTargetingValue | stringTargetingValue - } - - def "PBS should be able to match site FPD targeting taken from different sources by IN matching function"() { - given: "Bid request with set site FPD in different request places" - def siteLanguage = PBSUtils.randomString - def appLanguage = PBSUtils.randomString - def doohLanguage = PBSUtils.randomString - def impLanguage = PBSUtils.randomString - def bidRequest = getSiteFpdBidRequest(siteLanguage, appLanguage, doohLanguage, impLanguage) - - and: "Planner response with IN all of site FPD values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, IN, [siteLanguage, appLanguage, impLanguage, PBSUtils.randomString]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should be able to match user FPD targeting taken from different sources by MATCHES matching function"() { - given: "Bid request with set user FPD in different request places" - def bidRequest = getUserFpdBidRequest(userBuyerUid, userExtDataBuyerUid) - - and: "Planner response with MATCHES 1 of user FPD values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemSize = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize - - where: - userBuyerUid | userExtDataBuyerUid - stringTargetingValue | PBSUtils.randomString - PBSUtils.randomString | stringTargetingValue - } - - def "PBS should be able to match user FPD targeting taken from different sources by IN matching function"() { - given: "Bid request with set user FPD in different request places" - def userBuyerUid = PBSUtils.randomString - def userExtDataBuyerUid = PBSUtils.randomString - def bidRequest = getUserFpdBidRequest(userBuyerUid, userExtDataBuyerUid) - - and: "Planner response with IN all of user FPD values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, IN, [userBuyerUid, userExtDataBuyerUid, PBSUtils.randomString]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemSize = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize - } - - def "PBS should support targeting by SITE First Party Data when request ext prebid bidder config is given"() { - given: "Bid request with set Site specific bidder config" - def bidRequest = BidRequest.defaultBidRequest.tap { - def site = new Site().tap { - ext = new SiteExt(data: new SiteExtData(language: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [GENERIC], - config: new BidderConfig(ortb2: new BidderConfigOrtb(site: site))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, matchingFunction, matchingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - matchingFunction | matchingValue - INTERSECTS | [stringTargetingValue, PBSUtils.randomString] - MATCHES | stringTargetingValue - } - - def "PBS should support targeting by USER First Party Data when request ext prebid bidder config is given"() { - given: "Bid request with set User specific bidder config" - def bidRequest = BidRequest.defaultBidRequest.tap { - def user = new User().tap { - ext = new UserExt(data: new UserExtData(buyeruid: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [GENERIC], - config: new BidderConfig(ortb2: new BidderConfigOrtb(user: user))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, matchingFunction, matchingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - matchingFunction | matchingValue - INTERSECTS | [stringTargetingValue, PBSUtils.randomString] - MATCHES | stringTargetingValue - } - - def "PBS shouldn't target by SITE First Party Data when request ext prebid bidder config with not matched bidder is given"() { - given: "Bid request with request not matched bidder" - def notMatchedBidder = APPNEXUS - def bidRequest = BidRequest.defaultBidRequest.tap { - def site = new Site().tap { - ext = new SiteExt(data: new SiteExtData(language: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [notMatchedBidder], - config: new BidderConfig(ortb2: new BidderConfigOrtb(site: site))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, matchingFunction, matchingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't had PG auction" - assert !auctionResponse.ext?.debug?.pgmetrics - - where: - matchingFunction | matchingValue - INTERSECTS | [stringTargetingValue, PBSUtils.randomString] - MATCHES | stringTargetingValue - } - - def "PBS shouldn't target by USER First Party Data when request ext prebid bidder config with not matched bidder is given"() { - given: "Bid request with request not matched bidder" - def notMatchedBidder = APPNEXUS - def bidRequest = BidRequest.defaultBidRequest.tap { - def user = new User().tap { - ext = new UserExt(data: new UserExtData(buyeruid: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [notMatchedBidder], - config: new BidderConfig(ortb2: new BidderConfigOrtb(user: user))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, matchingFunction, matchingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't had PG auction" - assert !auctionResponse.ext?.debug?.pgmetrics - - where: - matchingFunction | matchingValue - INTERSECTS | [stringTargetingValue, PBSUtils.randomString] - MATCHES | stringTargetingValue - } - - def "PBS should support targeting by SITE First Party Data when a couple of request ext prebid bidder configs are given"() { - given: "Bid request with 1 not matched Site specific bidder config and 1 matched" - def bidRequest = BidRequest.defaultBidRequest.tap { - def bidderConfigSite = new Site().tap { - ext = new SiteExt(data: new SiteExtData(language: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [GENERIC], - config: new BidderConfig(ortb2: new BidderConfigOrtb(site: bidderConfigSite))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SFPD_LANGUAGE, matchingFunction, matchingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - matchingFunction | matchingValue - INTERSECTS | [stringTargetingValue, PBSUtils.randomString] - MATCHES | stringTargetingValue - } - - def "PBS should support targeting by USER First Party Data when a couple of request ext prebid bidder configs are given"() { - given: "Bid request with 1 not matched User specific bidder config and 1 matched" - def bidRequest = BidRequest.defaultBidRequest.tap { - def bidderConfigUser = new User().tap { - ext = new UserExt(data: new UserExtData(buyeruid: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [GENERIC], - config: new BidderConfig(ortb2: new BidderConfigOrtb(user: bidderConfigUser))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, matchingFunction, matchingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - matchingFunction | matchingValue - INTERSECTS | [stringTargetingValue, PBSUtils.randomString] - MATCHES | stringTargetingValue - } - - def "PBS shouldn't rely on bidder name case strategy when bidder name in another case stately"() { - given: "Bid request with 1 not matched User specific bidder config and 1 matched" - def bidRequest = BidRequest.defaultBidRequest.tap { - def bidderConfigUser = new User().tap { - ext = new UserExt(data: new UserExtData(buyeruid: stringTargetingValue)) - } - def bidderConfig = new ExtPrebidBidderConfig(bidders: [bidders], - config: new BidderConfig(ortb2: new BidderConfigOrtb(user: bidderConfigUser))) - ext = new BidRequestExt(prebid: new Prebid(debug: 1, bidderConfig: [bidderConfig])) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(UFPD_BUYER_UID, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - bidders << [GENERIC, GENERIC_CAMEL_CASE] - } - - private BidRequest getSiteFpdBidRequest(String siteLanguage, String appLanguage, String doohLanguage, String impLanguage) { - def bidRequest - if (siteLanguage != null) { - bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { - site.ext = new SiteExt(data: new SiteExtData(language: siteLanguage)) - } - } else if (appLanguage != null) { - bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - app.ext = new AppExt(data: new AppExtData(language: appLanguage)) - } - } else { - bidRequest = BidRequest.getDefaultBidRequest(DOOH).tap { - dooh.ext = new DoohExt(data: new DoohExtData(language: doohLanguage)) - } - } - bidRequest.imp[0].tap { - ext.context = new ImpExtContext(data: new ImpExtContextData(language: impLanguage)) - } - bidRequest - } - - private BidRequest getUserFpdBidRequest(String userBuyerUid, String userExtDataBuyerUid) { - BidRequest.defaultBidRequest.tap { - user = User.defaultUser.tap { - buyeruid = userBuyerUid - ext = new UserExt(data: new UserExtData(buyeruid: userExtDataBuyerUid)) - } - } - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy deleted file mode 100644 index 882860a9e5b..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy +++ /dev/null @@ -1,560 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.bidder.Rubicon -import org.prebid.server.functional.model.deals.lineitem.LineItemSize -import org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator -import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.App -import org.prebid.server.functional.model.request.auction.Banner -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.auction.Bidder -import org.prebid.server.functional.model.request.auction.Device -import org.prebid.server.functional.model.request.auction.Geo -import org.prebid.server.functional.model.request.auction.GeoExt -import org.prebid.server.functional.model.request.auction.GeoExtGeoProvider -import org.prebid.server.functional.model.request.auction.Imp -import org.prebid.server.functional.model.request.auction.ImpExt -import org.prebid.server.functional.model.request.auction.ImpExtContext -import org.prebid.server.functional.model.request.auction.ImpExtContextData -import org.prebid.server.functional.model.request.auction.ImpExtContextDataAdServer -import org.prebid.server.functional.model.request.auction.Publisher -import org.prebid.server.functional.model.request.auction.User -import org.prebid.server.functional.model.request.auction.UserExt -import org.prebid.server.functional.model.request.auction.UserTime -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Shared - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static java.time.ZoneOffset.UTC -import static java.time.temporal.WeekFields.SUNDAY_START -import static org.prebid.server.functional.model.bidder.BidderName.RUBICON -import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.NOT -import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.OR -import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.UPPERCASE_AND -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.IN -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.INTERSECTS -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.MATCHES -import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.WITHIN -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_AD_SLOT -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_MEDIA_TYPE -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_SIZE -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.APP_BUNDLE -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.BIDP_ACCOUNT_ID -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.DEVICE_METRO -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.DEVICE_REGION -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.DOW -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.HOUR -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.INVALID -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.PAGE_POSITION -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.REFERRER -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SITE_DOMAIN -import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_BUYER_UID -import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP -import static org.prebid.server.functional.model.response.auction.MediaType.BANNER -import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO - -class TargetingSpec extends BasePgSpec { - - @Shared - String stringTargetingValue = PBSUtils.randomString - @Shared - Integer integerTargetingValue = PBSUtils.randomNumber - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should invalidate line items when targeting has #reason"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = targeting - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't had PG deals auction as line item hasn't passed validation" - assert !auctionResponse.ext?.debug?.pgmetrics - - where: - reason | targeting - - "two root nodes" | Targeting.invalidTwoRootNodesTargeting - - "invalid boolean operator" | new Targeting.Builder(BooleanOperator.INVALID).addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) - .build() - - "uppercase boolean operator" | new Targeting.Builder(UPPERCASE_AND).addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) - .build() - - "invalid targeting type" | Targeting.defaultTargetingBuilder - .addTargeting(INVALID, INTERSECTS, [PBSUtils.randomString]) - .build() - - "'in' matching type value as not list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, IN, BANNER) - .build() - - "'intersects' matching type value as not list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, BANNER) - .build() - - "'within' matching type value as not list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, WITHIN, BANNER) - .build() - - "'matches' matching type value as list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, MATCHES, [BANNER]) - .build() - - "null targeting height and width" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [new LineItemSize(w: null, h: null)]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) - .build() - } - - def "PBS should invalidate line items with not supported '#matchingFunction' matching function by '#targetingType' targeting type"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(targetingType, matchingFunction, [PBSUtils.randomString]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't had PG deals auction as line item hasn't passed validation" - assert !auctionResponse.ext?.debug?.pgmetrics - - where: - matchingFunction | targetingType - INTERSECTS | SITE_DOMAIN - WITHIN | SITE_DOMAIN - INTERSECTS | REFERRER - WITHIN | REFERRER - INTERSECTS | APP_BUNDLE - WITHIN | APP_BUNDLE - INTERSECTS | AD_UNIT_AD_SLOT - WITHIN | AD_UNIT_AD_SLOT - } - - def "PBS should support line item targeting by string '#targetingType' targeting type"() { - given: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.getAccountId()).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(targetingType, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - targetingType | bidRequest - - REFERRER | BidRequest.defaultBidRequest.tap { - site.page = stringTargetingValue - } - - APP_BUNDLE | BidRequest.getDefaultBidRequest(APP).tap { - app = App.defaultApp.tap { - bundle = stringTargetingValue - } - } - - UFPD_BUYER_UID | BidRequest.defaultBidRequest.tap { - user = User.defaultUser.tap { - buyeruid = stringTargetingValue - } - } - } - - def "PBS should support targeting matching by bidder parameters"() { - given: "Bid request with specified bidder parameter" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - banner = Banner.defaultBanner - ext = ImpExt.defaultImpExt - ext.prebid.bidder = new Bidder(rubicon: Rubicon.defaultRubicon.tap { accountId = integerTargetingValue }) - }] - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].source = RUBICON.name().toLowerCase() - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(BIDP_ACCOUNT_ID, INTERSECTS, [integerTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should support line item targeting by page position targeting type"() { - given: "Bid request and bid response" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].banner.pos = integerTargetingValue - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(PAGE_POSITION, IN, [integerTargetingValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should support line item targeting by userdow targeting type"() { - given: "Bid request and bid response" - def bidRequest = BidRequest.defaultBidRequest.tap { - def weekDay = ZonedDateTime.now(ZoneId.from(UTC)).dayOfWeek.get(SUNDAY_START.dayOfWeek()) - user = User.defaultUser.tap { - ext = new UserExt(time: new UserTime(userdow: weekDay)) - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(DOW, IN, [ZonedDateTime.now(ZoneId.from(UTC)).dayOfWeek.get(SUNDAY_START.dayOfWeek())]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should support line item targeting by userhour targeting type"() { - given: "Bid request and bid response" - def bidRequest = BidRequest.defaultBidRequest.tap { - def hour = ZonedDateTime.now(ZoneId.from(UTC)).hour - user = User.defaultUser.tap { - ext = new UserExt(time: new UserTime(userhour: hour)) - } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(HOUR, IN, [ZonedDateTime.now(ZoneId.from(UTC)).hour]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } - - def "PBS should support line item targeting by '#targetingType' targeting type"() { - given: "Bid request and bid response" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(HOUR, IN, [ZonedDateTime.now(ZoneId.from(UTC)).hour]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - - where: - targetingType | targetingValue - - "'\$or' root node with one match" | new Targeting.Builder(OR).addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) - .build() - - "'\$not' root node without matches" | new Targeting.Builder(NOT).buildNotBooleanOperatorTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) - } - - def "PBS should support line item domain targeting by #domainTargetingType"() { - given: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SITE_DOMAIN, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemSize = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize - - and: "Targeting recorded as matched" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedDomainTargeting?.size() == lineItemSize - - where: - domainTargetingType | bidRequest - - "site domain" | BidRequest.defaultBidRequest.tap { - site.domain = stringTargetingValue - } - - "site publisher domain" | BidRequest.defaultBidRequest.tap { - site.publisher = Publisher.defaultPublisher.tap { domain = stringTargetingValue } - } - } - - def "PBS should support line item domain targeting"() { - given: "Bid response" - def bidRequest = BidRequest.defaultBidRequest.tap { - site.domain = siteDomain - site.publisher = Publisher.defaultPublisher.tap { domain = sitePublisherDomain } - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(SITE_DOMAIN, IN, [siteDomain]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemSize = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize - - and: "Targeting recorded as matched" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedDomainTargeting?.size() == lineItemSize - - where: - siteDomain | sitePublisherDomain - "www.example.com" | null - "https://www.example.com" | null - "www.example.com" | "example.com" - } - - def "PBS should appropriately match '\$or', '\$not' line items targeting root node rules"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = targeting - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't had PG deals auction as targeting differs" - assert !auctionResponse.ext?.debug?.pgmetrics - - where: - targeting << [new Targeting.Builder(OR).addTargeting(AD_UNIT_SIZE, INTERSECTS, [new LineItemSize(w: PBSUtils.randomNumber, h: PBSUtils.randomNumber)]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) - .build(), - new Targeting.Builder(NOT).buildNotBooleanOperatorTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize])] - } - - def "PBS should support line item targeting by device geo region, metro when request region, metro as int or str value are given"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest.tap { - device = new Device(geo: new Geo(ext: new GeoExt(geoProvider: new GeoExtGeoProvider(region: requestValue, - metro: requestValue)))) - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(DEVICE_REGION, IN, [lineItemValue]) - .addTargeting(DEVICE_METRO, IN, [lineItemValue]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - assert auctionResponse.ext.debug.pgmetrics.matchedWholeTargeting.first() == plansResponse.lineItems.first().lineItemId - - where: - requestValue | lineItemValue - stringTargetingValue | stringTargetingValue - integerTargetingValue | integerTargetingValue as String - } - - def "PBS should be able to match Ad Slot targeting taken from different sources by MATCHES matching function"() { - given: "Bid request with set ad slot info in different request places" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - tagId = impTagId - ext.gpid = impExtGpid - ext.data = new ImpExtContextData(pbAdSlot: adSlot, - adServer: new ImpExtContextDataAdServer(adSlot: adServerAdSlot)) - }] - } - - and: "Planner response with MATCHES one of Ad Slot values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(AD_UNIT_AD_SLOT, MATCHES, stringTargetingValue) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemSize = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize - - where: - impTagId | impExtGpid | adSlot | adServerAdSlot - stringTargetingValue | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString - PBSUtils.randomString | stringTargetingValue | PBSUtils.randomString | PBSUtils.randomString - null | null | stringTargetingValue | PBSUtils.randomString - null | null | PBSUtils.randomString | stringTargetingValue - } - - def "PBS should be able to match Ad Slot targeting taken from different sources by IN matching function"() { - given: "Bid request with set ad slot info in different request places" - def contextAdSlot = PBSUtils.randomString - def contextAdServerAdSlot = PBSUtils.randomString - def adSlot = PBSUtils.randomString - def adServerAdSlot = PBSUtils.randomString - def bidRequest = BidRequest.defaultBidRequest.tap { - imp = [Imp.defaultImpression.tap { - ext.context = new ImpExtContext(data: new ImpExtContextData(pbAdSlot: contextAdSlot, - adServer: new ImpExtContextDataAdServer(adSlot: contextAdServerAdSlot))) - ext.data = new ImpExtContextData(pbAdSlot: adSlot, - adServer: new ImpExtContextDataAdServer(adSlot: adServerAdSlot)) - }] - } - - and: "Planner response with IN all of Ad Slot values" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = Targeting.defaultTargetingBuilder - .addTargeting(AD_UNIT_AD_SLOT, IN, [contextAdSlot, contextAdServerAdSlot, adSlot, adServerAdSlot, PBSUtils.randomString]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - def lineItemSize = plansResponse.lineItems.size() - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize - } - - def "PBS should be able to match video size targeting taken from imp[].video sources by INTERSECTS matching function"() { - given: "Default video bid request" - def lineItemSize = LineItemSize.defaultLineItemSize - def bidRequest = BidRequest.defaultVideoRequest.tap { - imp[0].video.w = lineItemSize.w - imp[0].video.h = lineItemSize.h - } - - and: "Planner response" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].targeting = new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [lineItemSize]) - .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) - .build() - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS had PG auction" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/TokenSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/TokenSpec.groovy deleted file mode 100644 index 672db59e438..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/TokenSpec.groovy +++ /dev/null @@ -1,317 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.deals.lineitem.LineItem -import org.prebid.server.functional.model.deals.lineitem.Token -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.HttpUtil - -import java.time.ZoneId -import java.time.ZonedDateTime - -import static java.time.ZoneOffset.UTC -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC - -class TokenSpec extends BasePgSpec { - - def cleanup() { - pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) - } - - def "PBS should start using line item in auction when its expired tokens number is increased"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock zero tokens line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].tokens[0].total = 0 - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is requested" - def firstAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start processing PG deals" - assert firstAuctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == - [plansResponse.lineItems[0].lineItemId] as Set - assert !firstAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder - - when: "Line item tokens are updated" - plansResponse.lineItems[0].deliverySchedules[0].tokens[0].total = 1 - plansResponse.lineItems[0].deliverySchedules[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(1) - generalPlanner.initPlansResponse(plansResponse) - - and: "Updated line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Auction is requested for the second time" - bidder.setResponse(bidRequest.id, bidResponse) - def secondAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS should process PG deals" - def sentToBidder = secondAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == plansResponse.lineItems.size() - assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId - } - - def "PBS shouldn't allow line items with zero token number take part in auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock zero tokens line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].tokens[0].total = 0 - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS should recognize line items with pacing deferred" - assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == [plansResponse.lineItems[0].lineItemId] as Set - } - - def "PBS should allow line item take part in auction when it has at least one unspent token among all expired tokens"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line item with zero and 1 available tokens" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - def deliverySchedules = lineItems[0].deliverySchedules[0] - deliverySchedules.tokens[0].total = 0 - deliverySchedules.tokens << new Token(priorityClass: 2, total: 0) - deliverySchedules.tokens << new Token(priorityClass: 3, total: 1) - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - def lineItemCount = plansResponse.lineItems.size() - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS should process PG deals" - assert !auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred - assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItemCount - def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == lineItemCount - assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId - } - - def "PBS shouldn't allow line item take part in auction when all its tokens are spent"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock with 1 token to spend line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].tokens[0].total = 1 - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Auction is happened for the first time" - pgPbsService.sendAuctionRequest(bidRequest) - - when: "Requesting auction for the second time" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start PG processing" - assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == [plansResponse.lineItems[0].lineItemId] as Set - } - - def "PBS should take only the first token among tokens with the same priority class"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line item with 2 tokens of the same priority but the first has zero total number" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - def tokens = [new Token(priorityClass: 1, total: 0), new Token(priorityClass: 1, total: 1)] - lineItems[0].deliverySchedules[0].tokens = tokens - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is happened" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start PG processing as it was processed only the first token with 0 total number" - assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == [plansResponse.lineItems[0].lineItemId] as Set - } - - def "PBS shouldn't allow line item take part in auction when its number of available impressions is ahead of the scheduled time"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line item to have max 2 impressions during one week" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].tokens[0].total = 2 - lineItems[0].startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) - lineItems[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) - lineItems[0].endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).plusWeeks(1) - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is requested for the first time" - def firstAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS processed PG deals" - def sentToBidder = firstAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == plansResponse.lineItems.size() - assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId - - when: "Auction is requested for the second time" - def secondAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS hasn't allowed line item take part in auction as it has only one impression left to be shown during the week" - assert secondAuctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == - [plansResponse.lineItems[0].lineItemId] as Set - assert !secondAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder - } - - def "PBS should abandon line item with updated zero available token number take part in auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock with not null tokens number line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].tokens[0].total = 2 - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - when: "Auction is requested" - def firstAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS processed PG deals" - def sentToBidder = firstAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) - assert sentToBidder?.size() == plansResponse.lineItems.size() - assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId - - when: "Line item tokens are updated to have no available tokens" - plansResponse.lineItems[0].deliverySchedules[0].tokens[0].total = 0 - plansResponse.lineItems[0].deliverySchedules[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) - - generalPlanner.initPlansResponse(plansResponse) - - and: "Updated line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Auction is requested for the second time" - def secondAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) - - then: "PBS shouldn't start processing PG deals" - assert secondAuctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == - [plansResponse.lineItems[0].lineItemId] as Set - assert !secondAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder - } - - def "PBS should ignore line item pacing when ignore pacing header is present in the request"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock zero tokens line item" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].deliverySchedules[0].tokens[0].total = 0 - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Pg ignore pacing header" - def pgIgnorePacingHeader = ["${HttpUtil.PG_IGNORE_PACING_HEADER}": "1"] - - when: "Auction is requested" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, pgIgnorePacingHeader) - - then: "PBS should process PG deals" - def pgMetrics = auctionResponse.ext?.debug?.pgmetrics - def sentToBidder = pgMetrics?.sentToBidder[GENERIC.value] - assert sentToBidder?.size() == plansResponse.lineItems.size() - assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId - assert pgMetrics.readyToServe == [plansResponse.lineItems[0].lineItemId] as Set - } - - def "PBS should prioritize line item when pg ignore pacing and #reason"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock with additional lineItem" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems.add(updateLineItem(bidRequest.site.publisher.id)) - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Pg ignore pacing header" - def pgIgnorePacingHeader = ["${HttpUtil.PG_IGNORE_PACING_HEADER}": "1"] - - when: "Auction is requested" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, pgIgnorePacingHeader) - - then: "PBS should process PG deals" - def pgMetrics = auctionResponse.ext?.debug?.pgmetrics - assert pgMetrics?.readyToServe?.size() == plansResponse.lineItems.size() - assert pgMetrics.readyToServe.eachWithIndex { id, index -> - id == plansResponse.lineItems[index].lineItemId } - - where: - reason | updateLineItem - "cpm is null" | {siteId -> LineItem.getDefaultLineItem(siteId).tap { - price.cpm = null - }} - "relative priority is null" | {siteId -> LineItem.getDefaultLineItem(siteId).tap { - relativePriority = null - }} - "no tokens unspent" | {siteId -> LineItem.getDefaultLineItem(siteId).tap { - deliverySchedules[0].tokens[0].total = 0 - }} - "delivery plan is null" | {siteId -> LineItem.getDefaultLineItem(siteId).tap { - deliverySchedules = null - }} - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/UserDetailsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/UserDetailsSpec.groovy deleted file mode 100644 index a61907af8ae..00000000000 --- a/src/test/groovy/org/prebid/server/functional/tests/pg/UserDetailsSpec.groovy +++ /dev/null @@ -1,343 +0,0 @@ -package org.prebid.server.functional.tests.pg - -import org.prebid.server.functional.model.UidsCookie -import org.prebid.server.functional.model.deals.lineitem.FrequencyCap -import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse -import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse -import org.prebid.server.functional.model.mock.services.httpsettings.HttpAccountsResponse -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.request.event.EventRequest -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.testcontainers.Dependencies -import org.prebid.server.functional.testcontainers.scaffolding.HttpSettings -import org.prebid.server.functional.util.HttpUtil -import org.prebid.server.functional.util.PBSUtils -import spock.lang.Shared - -import java.time.format.DateTimeFormatter - -import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 -import static org.mockserver.model.HttpStatusCode.NOT_FOUND_404 -import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.deals.lineitem.LineItem.TIME_PATTERN - -class UserDetailsSpec extends BasePgSpec { - - private static final String USER_SERVICE_NAME = "userservice" - - @Shared - HttpSettings httpSettings = new HttpSettings(Dependencies.networkServiceContainer) - - def "PBS should send user details request to the User Service during deals auction"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial user details request count is taken" - def initialRequestCount = userData.recordedUserDetailsRequestCount - - and: "Cookies with user ids" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS sends a request to the User Service" - def updatedRequestCount = userData.recordedUserDetailsRequestCount - assert updatedRequestCount == initialRequestCount + 1 - - and: "Request corresponds to the payload" - def userDetailsRequest = userData.recordedUserDetailsRequest - assert userDetailsRequest.ids?.size() == 1 - assert userDetailsRequest.ids[0].id == uidsCookie.tempUIDs.get(GENERIC).uid - assert userDetailsRequest.ids[0].type == pgConfig.userIdType - } - - def "PBS should validate bad user details response status code #statusCode"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "User Service response is set" - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse, statusCode) - - and: "Initial user details request count is taken" - def initialRequestCount = userData.recordedUserDetailsRequestCount - - and: "Cookies with user ids" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS sends a request to the User Service during auction" - assert userData.recordedUserDetailsRequestCount == initialRequestCount + 1 - def userServiceCall = auctionResponse.ext?.debug?.httpcalls?.get(USER_SERVICE_NAME) - assert userServiceCall?.size() == 1 - - assert !userServiceCall[0].status - assert !userServiceCall[0].responseBody - - cleanup: - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) - - where: - statusCode << [NO_CONTENT_204, NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] - } - - def "PBS should invalidate user details response body when response has absent #fieldName field"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial user details request count is taken" - def initialRequestCount = userData.recordedUserDetailsRequestCount - - and: "User Service response is set" - userData.setUserDataResponse(userDataResponse) - - and: "Cookies with user ids" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS sends a request to the User Service" - assert userData.recordedUserDetailsRequestCount == initialRequestCount + 1 - - and: "Call to the user service was made" - assert auctionResponse.ext?.debug?.httpcalls?.get(USER_SERVICE_NAME)?.size() == 1 - - and: "Data from the user service response wasn't added to the bid request by PBS" - assert !auctionResponse.ext?.debug?.resolvedRequest?.user?.data - assert !auctionResponse.ext?.debug?.resolvedRequest?.user?.ext?.fcapids - - cleanup: - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) - - where: - fieldName | userDataResponse - "user" | new UserDetailsResponse(user: null) - "user.data" | UserDetailsResponse.defaultUserResponse.tap { user.data = null } - "user.ext" | UserDetailsResponse.defaultUserResponse.tap { user.ext = null } - } - - def "PBS should abandon line items with user fCap ids take part in auction when user details response failed"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Planner Mock line items with added frequency cap" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { - lineItems[0].frequencyCaps = [FrequencyCap.defaultFrequencyCap.tap { fcapId = PBSUtils.randomNumber as String }] - } - generalPlanner.initPlansResponse(plansResponse) - - and: "Bid response" - def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Bad User Service Response is set" - userData.setUserDataResponse(new UserDetailsResponse(user: null)) - - and: "Cookies header" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS" - def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) - - then: "PBS hasn't started processing PG deals as line item targeting frequency capped lookup failed" - assert auctionResponse.ext?.debug?.pgmetrics?.matchedTargetingFcapLookupFailed?.size() == - plansResponse.lineItems.size() - - cleanup: - userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) - } - - def "PBS should send win notification request to the User Service on bidder wins"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - def lineItemId = plansResponse.lineItems[0].lineItemId - def lineItemUpdateTime = plansResponse.lineItems[0].updatedTimeStamp - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial win notification request count" - def initialRequestCount = userData.requestCount - - and: "Enabled event request" - def winEventRequest = EventRequest.defaultEventRequest.tap { - it.lineItemId = lineItemId - analytics = 0 - } - - and: "Default account response" - def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(winEventRequest.accountId.toString()) - httpSettings.setResponse(winEventRequest.accountId.toString(), httpSettingsResponse) - - and: "Cookies header" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS where the winner is instantiated" - pgPbsService.sendAuctionRequest(bidRequest) - - and: "Sending event request to PBS" - pgPbsService.sendEventRequest(winEventRequest, cookieHeader) - - then: "PBS sends a win notification to the User Service" - PBSUtils.waitUntil { userData.requestCount == initialRequestCount + 1 } - - and: "Win request corresponds to the payload" - def timeFormatter = DateTimeFormatter.ofPattern(TIME_PATTERN) - - verifyAll(userData.recordedWinEventRequest) { winNotificationRequest -> - winNotificationRequest.bidderCode == GENERIC.value - winNotificationRequest.bidId == winEventRequest.bidId - winNotificationRequest.lineItemId == lineItemId - winNotificationRequest.region == pgConfig.region - winNotificationRequest.userIds?.size() == 1 - winNotificationRequest.userIds[0].id == uidsCookie.tempUIDs.get(GENERIC).uid - winNotificationRequest.userIds[0].type == pgConfig.userIdType - timeFormatter.format(winNotificationRequest.lineUpdatedDateTime) == timeFormatter.format(lineItemUpdateTime) - winNotificationRequest.winEventDateTime.isAfter(winNotificationRequest.lineUpdatedDateTime) - !winNotificationRequest.frequencyCaps - } - } - - def "PBS shouldn't send win notification request to the User Service when #reason line item id is given"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock line items" - generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial win notification request count" - def initialRequestCount = userData.requestCount - - and: "Enabled event request" - def eventRequest = EventRequest.defaultEventRequest.tap { - it.lineItemId = lineItemId - analytics = 0 - } - - and: "Default account response" - def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(eventRequest.accountId.toString()) - httpSettings.setResponse(eventRequest.accountId.toString(), httpSettingsResponse) - - and: "Cookies header" - def uidsCookie = UidsCookie.defaultUidsCookie - def cookieHeader = HttpUtil.getCookieHeader(uidsCookie) - - when: "Sending auction request to PBS where the winner is instantiated" - pgPbsService.sendAuctionRequest(bidRequest) - - and: "Sending event request to PBS" - pgPbsService.sendEventRequest(eventRequest, cookieHeader) - - then: "PBS hasn't sent a win notification to the User Service" - assert userData.requestCount == initialRequestCount - - where: - reason | lineItemId - "null" | null - "non-existent" | PBSUtils.randomNumber as String - } - - def "PBS shouldn't send win notification request to the User Service when #reason cookies header was given"() { - given: "Bid request" - def bidRequest = BidRequest.defaultBidRequest - - and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - bidder.setResponse(bidRequest.id, bidResponse) - - and: "Planner Mock line items" - def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) - def lineItemId = plansResponse.lineItems[0].lineItemId - generalPlanner.initPlansResponse(plansResponse) - - and: "Line items are fetched by PBS" - updateLineItemsAndWait() - - and: "Initial win notification request count" - def initialRequestCount = userData.requestCount - - and: "Enabled event request" - def eventRequest = EventRequest.defaultEventRequest.tap { - it.lineItemId = lineItemId - analytics = 0 - } - - and: "Default account response" - def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(eventRequest.accountId.toString()) - httpSettings.setResponse(eventRequest.accountId.toString(), httpSettingsResponse) - - when: "Sending auction request to PBS where the winner is instantiated" - pgPbsService.sendAuctionRequest(bidRequest) - - and: "Sending event request to PBS" - pgPbsService.sendEventRequest(eventRequest, HttpUtil.getCookieHeader(uidsCookie)) - - then: "PBS hasn't sent a win notification to the User Service" - assert userData.requestCount == initialRequestCount - - where: - reason | uidsCookie - - "empty cookie" | new UidsCookie() - - "empty uids cookie" | UidsCookie.defaultUidsCookie.tap { - uids = null - tempUIDs = null - } - } -} diff --git a/src/test/groovy/org/prebid/server/functional/tests/postgres/PostgresBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/postgres/PostgresBaseSpec.groovy new file mode 100644 index 00000000000..05b5e054cc0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/postgres/PostgresBaseSpec.groovy @@ -0,0 +1,38 @@ +package org.prebid.server.functional.tests.postgres + +import org.prebid.server.functional.repository.HibernateRepositoryService +import org.prebid.server.functional.repository.dao.AccountDao +import org.prebid.server.functional.repository.dao.StoredImpDao +import org.prebid.server.functional.repository.dao.StoredRequestDao +import org.prebid.server.functional.repository.dao.StoredResponseDao +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.Dependencies +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.tests.BaseSpec +import org.testcontainers.lifecycle.Startables + +class PostgresBaseSpec extends BaseSpec { + + protected static HibernateRepositoryService repository + protected static AccountDao accountDao + protected static StoredImpDao storedImpDao + protected static StoredRequestDao storedRequestDao + protected static StoredResponseDao storedResponseDao + + protected PrebidServerService pbsServiceWithPostgres + + void setup() { + Startables.deepStart(Dependencies.postgresqlContainer) + .join() + repository = new HibernateRepositoryService(Dependencies.postgresqlContainer) + accountDao = repository.accountDao + storedImpDao = repository.storedImpDao + storedRequestDao = repository.storedRequestDao + storedResponseDao = repository.storedResponseDao + pbsServiceWithPostgres = pbsServiceFactory.getService(PbsConfig.postgreSqlConfig) + } + + void cleanup() { + Dependencies.postgresqlContainer.stop() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/postgres/PostgresDBSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/postgres/PostgresDBSpec.groovy new file mode 100644 index 00000000000..fee381bad7c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/postgres/PostgresDBSpec.groovy @@ -0,0 +1,49 @@ +package org.prebid.server.functional.tests.postgres + +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.util.PBSUtils + +class PostgresDBSpec extends PostgresBaseSpec { + + def "PBS with postgresql should proceed with stored requests and responses correctly"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request with specified stored response" + def storedResponseId = PBSUtils.randomNumber + def ampStoredRequest = BidRequest.defaultStoredRequest + ampStoredRequest.imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + ampStoredRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Create and save account in the DB" + def account = new Account(uuid: ampRequest.account) + accountDao.save(account) + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(ampStoredRequest) + storedImpDao.save(storedImp) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Stored response in DB" + def storedAuctionResponse = SeatBid.getStoredResponse(ampStoredRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes amp request" + def response = pbsServiceWithPostgres.sendAmpRequest(ampRequest) + + then: "PBS should not reject request" + assert response.ext?.debug?.httpcalls + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index a8500b07d83..71c7621eb20 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountPriceFloorsConfig @@ -43,7 +44,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { private static final int CURRENCY_CONVERSION_PRECISION = 3 private static final int FLOOR_VALUE_PRECISION = 4 - protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG) + protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG) def setupSpec() { floorsProvider.setResponse() @@ -105,24 +106,28 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { BidRequest.defaultBidRequest.tap { imp[0].video = Video.defaultVideo } } - protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, - BidRequest bidRequest, - BigDecimal expectedFloorValue) { - PBSUtils.waitUntil({ pbsService.sendAuctionRequest(bidRequest).ext.debug.resolvedRequest.imp[0].bidFloor == expectedFloorValue }, + protected void cacheFloorsProviderRules(BidRequest bidRequest, + BigDecimal expectedFloorValue, + PrebidServerService pbsService = floorsPbsService, + BidderName bidderName = BidderName.GENERIC) { + PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value].first.imp[0].bidFloor == expectedFloorValue }, 5000, 1000) } - protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, BidRequest bidRequest) { - PBSUtils.waitUntil({ pbsService.sendAuctionRequest(bidRequest).ext?.debug?.resolvedRequest?.ext?.prebid?.floors?.fetchStatus != INPROGRESS }, + protected void cacheFloorsProviderRules(BidRequest bidRequest, + PrebidServerService pbsService = floorsPbsService, + BidderName bidderName = BidderName.GENERIC) { + PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value]?.first?.ext?.prebid?.floors?.fetchStatus != INPROGRESS }, 5000, 1000) } - protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, - AmpRequest ampRequest, - BigDecimal expectedFloorValue) { - PBSUtils.waitUntil({ pbsService.sendAmpRequest(ampRequest).ext.debug.resolvedRequest.imp[0].bidFloor == expectedFloorValue }, + protected void cacheFloorsProviderRules(AmpRequest ampRequest, + BigDecimal expectedFloorValue, + PrebidServerService pbsService = floorsPbsService, + BidderName bidderName = BidderName.GENERIC) { + PBSUtils.waitUntil({ getRequests(pbsService.sendAmpRequest(ampRequest))[bidderName.value].first.imp[0].bidFloor == expectedFloorValue }, 5000, 1000) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index 8c7ad436813..581a71644a5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -1,6 +1,8 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.config.AccountPriceFloorsConfig +import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.auction.ImpExtPrebidFloors @@ -60,7 +62,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(currencyFloorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, currencyFloorsPbsService) when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -92,7 +94,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(currencyFloorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, currencyFloorsPbsService) and: "Get currency rates" def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() @@ -147,7 +149,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(currencyFloorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, currencyFloorsPbsService) when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -191,7 +193,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) and: "Flush metrics" flushMetrics(pbsService) @@ -238,8 +240,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { - config.auction.priceFloors.fetch.enabled = false + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.enabled = priceFloors + config.auction.priceFloorsSnakeCase = new AccountPriceFloorsConfig(enabled: true, + fetch: new PriceFloorsFetch(url: basicFetchUrl + bidRequest.accountId, enabled: priceFloorsSnakeCase)) } accountDao.save(account) @@ -253,6 +257,11 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { imp[0].bidFloorCur == floorCur ext?.prebid?.floors?.fetchStatus == NONE } + + where: + priceFloors | priceFloorsSnakeCase + false | null + null | false } def "PBS should prefer ext.prebid.floors for setting bidFloor, bidFloorCur for signalling"() { @@ -307,7 +316,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(currencyFloorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, currencyFloorsPbsService) and: "Get currency rates" def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() @@ -367,7 +376,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(currencyFloorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, currencyFloorsPbsService) and: "Flush metrics" flushMetrics(currencyFloorsPbsService) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy index 82efca80f45..4d47947670a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy @@ -19,8 +19,13 @@ import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.EMPTY import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { @@ -111,7 +116,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) and: "Bid response with 2 bids: price = floorValue, price < floorValue" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -163,7 +168,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) and: "Bid response with 2 bids: price = floorValue, price < floorValue" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -214,6 +219,531 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { assert response.seatbid?.first()?.bid?.collect { it.price } == [floorValue] } + def "PBS should remove imp floors information when data.noFloorSignalBidders contain original bidder name or wildcard"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + imp[0].bidFloor = floorValue + imp[0].bidFloorCur = floorCur + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + noFloorSignalBidders = [floorBidder] + } + } + } + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert !bidderImp.bidFloorCur + assert !bidderImp.bidFloor + + and: "Response should contain specific code and text in ext.warnings.prebid" + verifyAll(bidResponse.ext.warnings[PREBID]) { + it.code == [999] + it.message == ["noFloorSignal to bidder generic"] + } + + where: + floorBidder << [GENERIC, GENERIC_CAMEL_CASE, WILDCARD] + } + + def "PBS shouldn't remove imp floors information when data.noFloorSignalBidders contain empty bidder name"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + imp[0].bidFloor = floorValue + imp[0].bidFloorCur = floorCur + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + noFloorSignalBidders = floorBidders + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + + where: + floorBidders << [[EMPTY], [null], null] + } + + def "PBS shouldn't remove imp floors information when data.noFloorSignalBidders contain alias bidder"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.aliases = [(ALIAS.value): GENERIC] + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + imp[0].bidFloor = floorValue + imp[0].bidFloorCur = floorCur + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + noFloorSignalBidders = [floorBidder] + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + + where: + floorBidder << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should remove imp floors information when data.modelGroups[].noFloorSignalBidders contain bidder name or wildcard"() { + given: "Default BidRequest with floors" + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + modelGroups.first.noFloorSignalBidders = [floorBidder] + } + } + } + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert !bidderImp.bidFloorCur + assert !bidderImp.bidFloor + + and: "Response should contain specific code and text in ext.warnings.prebid" + verifyAll(bidResponse.ext.warnings[PREBID]) { + it.code == [999] + it.message == ["noFloorSignal to bidder generic"] + } + + where: + floorBidder << [GENERIC, GENERIC_CAMEL_CASE, WILDCARD] + } + + def "PBS shouldn't remove imp floors information when data.modelGroups[].noFloorSignalBidders contain empty bidder name"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + modelGroups[0].tap { + values = [(rule): floorValue] + noFloorSignalBidders = floorBidders + } + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + + where: + floorBidders << [[EMPTY], [null], null] + } + + def "PBS shouldn't remove imp floors information when data.modelGroups[].noFloorSignalBidders contain alias bidder"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.aliases = [(ALIAS.value): GENERIC] + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + modelGroups[0].tap { + values = [(rule): floorValue] + noFloorSignalBidders = [floorBidder] + } + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + + where: + floorBidder << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should remove imp floors information when enforcement.noFloorSignalBidders contain bidder name or wildcard"() { + given: "Default BidRequest with floors" + def bidRequest = bidRequestWithFloors.tap { + cur = [USD] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + enforcement = new ExtPrebidPriceFloorEnforcement(noFloorSignalBidders: [floorBidder]) + data.tap { + modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] + } + } + } + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert !bidderImp.bidFloorCur + assert !bidderImp.bidFloor + + and: "Response should contain specific code and text in ext.warnings.prebid" + verifyAll(bidResponse.ext.warnings[PREBID]) { + it.code == [999] + it.message == ["noFloorSignal to bidder generic"] + } + + where: + floorBidder << [GENERIC, GENERIC_CAMEL_CASE, WILDCARD] + } + + def "PBS shouldn't remove imp floors information when enforcement.noFloorSignalBidders contain empty bidder name"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + enforcement = new ExtPrebidPriceFloorEnforcement(noFloorSignalBidders: floorBidders) + data.tap { + modelGroups[0].values = [(rule): floorValue] + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + + where: + floorBidders << [[EMPTY], [null], null] + } + + def "PBS shouldn't remove imp floors information when enforcement.noFloorSignalBidders contain alias bidder"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.aliases = [(ALIAS.value): GENERIC] + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + enforcement = new ExtPrebidPriceFloorEnforcement(noFloorSignalBidders: [floorBidder]) + data.tap { + modelGroups[0].values = [(rule): floorValue] + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + + where: + floorBidder << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should prioritize data.modelGroups[].noFloorSignalBidders over data.noFloorSignalBidders and include imp floors when both are present"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + noFloorSignalBidders = [GENERIC] + modelGroups[0].tap { + values = [(rule): floorValue] + noFloorSignalBidders = [] + } + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + } + + def "PBS should prioritize data.modelGroups[].noFloorSignalBidders over data.noFloorSignalBidders and exclude imp floors when both are present"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + data = PriceFloorData.priceFloorData.tap { + noFloorSignalBidders = [] + modelGroups[0].tap { + values = [(rule): floorValue] + noFloorSignalBidders = [GENERIC] + } + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert !bidderImp.bidFloorCur + assert !bidderImp.bidFloor + + and: "Response should contain specific code and text in ext.warnings.prebid" + verifyAll(bidResponse.ext.warnings[PREBID]) { + it.code == [999] + it.message == ["noFloorSignal to bidder generic"] + } + } + + def "PBS should prioritize data.noFloorSignalBidders over enforcement.noFloorSignalBidders and include imp floors when both are present"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + enforcement = new ExtPrebidPriceFloorEnforcement(noFloorSignalBidders: [GENERIC]) + data = PriceFloorData.priceFloorData.tap { + noFloorSignalBidders = [] + modelGroups[0].values = [(rule): floorValue] + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.bidFloorCur == floorCur + assert bidderImp.bidFloor == floorValue + + and: "Response shouldn't contain any warnings" + assert !bidResponse.ext.warnings + } + + def "PBS should prioritize data.noFloorSignalBidders over enforcement.noFloorSignalBidders and exclude imp floors when both are present"() { + given: "Default BidRequest with floors" + def floorValue = PBSUtils.randomFloorValue + def floorCur = USD + def bidRequest = bidRequestWithFloors.tap { + cur = [floorCur] + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors.tap { + enforcement = new ExtPrebidPriceFloorEnforcement(noFloorSignalBidders: []) + data = PriceFloorData.priceFloorData.tap { + noFloorSignalBidders = [GENERIC] + modelGroups[0].values = [(rule): floorValue] + } + } + } + + and: "Bid response with price equal or bigger then in request" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.last().price = PBSUtils.getRandomFloorValue(floorValue) + cur = floorCur + } + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Account with enabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should remove imp.bidFlourCur and bidFloor from original request" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert !bidderImp.bidFloorCur + assert !bidderImp.bidFloor + + and: "Response should contain specific code and text in ext.warnings.prebid" + verifyAll(bidResponse.ext.warnings[PREBID]) { + it.code == [999] + it.message == ["noFloorSignal to bidder generic"] + } + } + def "PBS should prefer ext.prebid.floors for PF enforcement"() { given: "Default BidRequest with floors" def floorValue = PBSUtils.randomFloorValue @@ -252,7 +782,10 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { def "PBS should suppress deal that are below the matched floor when enforce-deal-floors = true"() { given: "Pbs with PF configuration with enforceDealFloors" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { - auction.priceFloors.enforceDealFloors = false + auction.priceFloors.tap { + enforceDealFloors = defaultAccountEnforeDealFloors + enforceDealFloorsSnakeCase = defaultAccountEnforeDealFloorsSnakeCase + } } def pbsService = pbsServiceFactory.getService(FLOORS_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]) @@ -265,7 +798,10 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Account with enabled fetch, fetch.url,enforceDealFloors in the DB" def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { - config.auction.priceFloors.enforceDealFloors = true + config.auction.priceFloors.tap { + enforceDealFloors = accountEnforeDealFloors + enforceDealFloorsSnakeCase = accountEnforeDealFloorsSnakeCase + } } accountDao.save(account) @@ -277,7 +813,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Bid response with 2 bids: bid.price = floorValue, dealBid.price < floorValue" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -296,6 +832,13 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { then: "PBS should suppress bid lower than floorRuleValue" assert response.seatbid?.first()?.bid?.collect { it.id } == [bidResponse.seatbid.first().bid.last().id] assert response.seatbid.first().bid.collect { it.price } == [floorValue] + + where: + defaultAccountEnforeDealFloors | defaultAccountEnforeDealFloorsSnakeCase | accountEnforeDealFloors | accountEnforeDealFloorsSnakeCase + false | null | true | null + null | false | null | true + null | false | true | null + false | null | null | true } def "PBS should not suppress deal that are below the matched floor according to ext.prebid.floors.enforcement.enforcePBS"() { @@ -326,7 +869,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Bid response with 2 bids: bid.price = floorValue, dealBid.price < floorValue" def dealBidPrice = floorValue - 0.1 @@ -382,7 +925,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Bid response with 2 bids: price = floorValue, price < floorValue" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -437,7 +980,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Bid response with 2 bids: price = floorValue, price < floorValue" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index d4c5529ea78..22e5b27a04f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -54,7 +54,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) @@ -81,7 +81,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) @@ -133,7 +133,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Account with enabled fetch, fetch.url, maxAgeSec in the DB" def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch.maxAgeSec = DEFAULT_MAX_AGE_SEC - 1 + config.auction.priceFloors.fetch = fetchConfig(bidRequest.app.publisher.id, DEFAULT_MAX_AGE_SEC - 1) } accountDao.save(account) @@ -146,6 +146,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() + + where: + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSec: max) }, + { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSecSnakeCase: max) }] } def "PBS should validate fetch.period-sec from account config"() { @@ -171,7 +175,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { where: fetchConfig << [{ int minPeriodSec, int maxAgeSec -> new PriceFloorsFetch(periodSec: minPeriodSec - 1) }, - { int minPeriodSec, int maxAgeSec -> new PriceFloorsFetch(periodSec: maxAgeSec + 1) }] + { int minPeriodSec, int maxAgeSec -> new PriceFloorsFetch(periodSec: maxAgeSec + 1) }, + { int minPeriodSec, int maxAgeSec -> new PriceFloorsFetch(periodSecSnakeCase: minPeriodSec - 1) }, + { int minPeriodSec, int maxAgeSec -> new PriceFloorsFetch(periodSecSnakeCase: maxAgeSec + 1) }] } def "PBS should validate fetch.max-file-size-kb from account config"() { @@ -187,7 +193,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) - then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 @@ -238,7 +244,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { where: fetchConfig << [{ int min, int max -> new PriceFloorsFetch(timeoutMs: min - 1) }, - { int min, int max -> new PriceFloorsFetch(timeoutMs: max + 1) }] + { int min, int max -> new PriceFloorsFetch(timeoutMs: max + 1) }, + { int min, int max -> new PriceFloorsFetch(timeoutMsSnakeCase: min - 1) }, + { int min, int max -> new PriceFloorsFetch(timeoutMsSnakeCase: max + 1) }] } def "PBS should validate fetch.enforce-floors-rate from account config"() { @@ -247,7 +255,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Account with enabled fetch, fetch.url, enforceFloorsRate in the DB" def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.enforceFloorsRate = enforceFloorsRate + config.auction.priceFloors.tap { + it.enforceFloorsRate = enforceFloorsRate + it.enforceFloorsRateSnakeCase = enforceFloorsRateSnakeCase + } } accountDao.save(account) @@ -262,7 +273,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert !response.seatbid?.isEmpty() where: - enforceFloorsRate << [PBSUtils.randomNegativeNumber, MAX_ENFORCE_FLOORS_RATE + 1] + enforceFloorsRate | enforceFloorsRateSnakeCase + PBSUtils.randomNegativeNumber | null + MAX_ENFORCE_FLOORS_RATE + 1 | null + null | PBSUtils.randomNegativeNumber + null | MAX_ENFORCE_FLOORS_RATE + 1 } def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() { @@ -317,7 +332,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def "PBS should fetch data from provider when use-dynamic-data = true"() { given: "Pbs with PF configuration with useDynamicData" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { - auction.priceFloors.useDynamicData = pbsConfigUseDynamicData + auction.priceFloors.tap { + useDynamicData = pbsConfigUseDynamicData + useDynamicDataSnakeCase = pbsConfigUseDynamicDataSnakeCase + } } def pbsService = pbsServiceFactory.getService(FLOORS_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]) @@ -329,8 +347,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Account with enabled fetch, fetch.url in the DB" def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch.enabled = true config.auction.priceFloors.useDynamicData = accountUseDynamicData + config.auction.priceFloors.useDynamicDataSnakeCase = accountUseDynamicDataSnakeCase } accountDao.save(account) @@ -342,7 +360,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) then: "PBS should fetch data from floors provider" assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 @@ -352,11 +370,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert bidderRequest.imp[0].bidFloor == floorValue where: - pbsConfigUseDynamicData | accountUseDynamicData - false | true - true | true - true | null - null | true + pbsConfigUseDynamicData | accountUseDynamicData | pbsConfigUseDynamicDataSnakeCase | accountUseDynamicDataSnakeCase + false | true | null | null + true | true | null | null + true | null | null | null + null | true | null | null + null | null | false | true + null | null | true | true + null | null | true | null + null | null | null | true } def "PBS should process floors from request when use-dynamic-data = false"() { @@ -377,7 +399,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) @@ -415,7 +437,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, BAD_REQUEST_400) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(floorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -468,7 +490,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " + @@ -601,7 +623,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert !response.seatbid?.isEmpty() } - def "PBS should log error and increase #FETCH_FAILURE_METRIC when Floors Provider response has more than fetch.max-rules"() { + def "PBS should log error and increase FETCH_FAILURE_METRIC when Floors Provider response has more than fetch.max-rules"() { given: "Test start time" def startTime = Instant.now() @@ -615,7 +637,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def accountId = bidRequest.app.publisher.id def maxRules = 1 def account = getAccountWithEnabledFetch(accountId).tap { - config.auction.priceFloors.fetch.maxRules = maxRules + config.auction.priceFloors.fetch = fetchConfig(accountId, maxRules) } accountDao.save(account) @@ -644,6 +666,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() + + where: + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRules: max) }, + { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRulesSnakeCase: max) }] } def "PBS should log error and increase #FETCH_FAILURE_METRIC when fetch request exceeds fetch.timeout-ms"() { @@ -687,7 +713,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert !response.seatbid?.isEmpty() } - def "PBS should log error and increase #FETCH_FAILURE_METRIC when Floors Provider's response size is more than fetch.max-file-size-kb"() { + def "PBS should log error and increase FETCH_FAILURE_METRIC when Floors Provider's response size is more than fetch.max-file-size-kb"() { given: "Test start time" def startTime = Instant.now() @@ -701,7 +727,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def accountId = bidRequest.app.publisher.id def maxSize = PBSUtils.getRandomNumber(1, 5) def account = getAccountWithEnabledFetch(accountId).tap { - config.auction.priceFloors.fetch.maxFileSizeKb = maxSize + config.auction.priceFloors.fetch = fetchConfig(accountId, maxSize) } accountDao.save(account) @@ -729,6 +755,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() + + where: + fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKb: maxKbSize) }, + { String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }] } def "PBS should prefer data from stored request when request doesn't contain floors data"() { @@ -774,9 +804,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } where: - request | accountId | bidRequestWithFloors - BidRequest.defaultBidRequest | request.site.publisher.id | bidRequestWithFloors - BidRequest.getDefaultBidRequest(APP) | request.app.publisher.id | getBidRequestWithFloors(APP) + request | accountId | bidRequestWithFloors + BidRequest.defaultBidRequest | request.site.publisher.id | bidRequestWithFloors + BidRequest.getDefaultBidRequest(APP) | request.app.publisher.id | getBidRequestWithFloors(APP) } def "PBS should prefer data from request when fetch is disabled in account config"() { @@ -881,7 +911,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) then: "Bidder request should contain floors data from floors provider" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -1366,7 +1396,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Set Floors Provider response with status code != 200" floorsProvider.setResponse(accountId, BAD_REQUEST_400) @@ -1499,7 +1529,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor data skipRate" + @@ -1558,7 +1588,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + @@ -1617,7 +1647,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup default" + @@ -1681,7 +1711,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse, header) and: "PBS cache rules" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) when: "PBS processes auction request" floorsPbsService.sendAuctionRequest(bidRequest) @@ -1714,7 +1744,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) when: "PBS processes auction request" floorsPbsService.sendAuctionRequest(bidRequest) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index b0c4a101279..478248d3f46 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -1,6 +1,9 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.bidderspecific.BidderRequest import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType @@ -23,7 +26,9 @@ import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.ChannelType.WEB +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.pricefloors.DeviceType.DESKTOP import static org.prebid.server.functional.model.pricefloors.DeviceType.MULTIPLE @@ -32,6 +37,7 @@ import static org.prebid.server.functional.model.pricefloors.DeviceType.TABLET import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.pricefloors.MediaType.VIDEO import static org.prebid.server.functional.model.pricefloors.PriceFloorField.AD_UNIT_CODE +import static org.prebid.server.functional.model.pricefloors.PriceFloorField.BIDDER import static org.prebid.server.functional.model.pricefloors.PriceFloorField.BOGUS import static org.prebid.server.functional.model.pricefloors.PriceFloorField.BUNDLE import static org.prebid.server.functional.model.pricefloors.PriceFloorField.CHANNEL @@ -49,7 +55,8 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Prebid.Channel -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { @@ -284,8 +291,8 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { def higherWidth = lowerWidth + 1 def higherHigh = lowerHigh + 1 def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].banner.format = [new Format(w: lowerWidth, h: lowerHigh), - new Format(w: higherWidth, h: higherHigh)] + imp[0].banner.format = [new Format(weight: lowerWidth, height: lowerHigh), + new Format(weight: higherWidth, height: higherHigh)] } and: "Account with enabled fetch, fetch.url in the DB" @@ -352,20 +359,20 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { mediaType | impClosure org.prebid.server.functional.model.response.auction.MediaType.BANNER | { int widthVal, int heightVal -> Imp.getDefaultImpression(mediaType).tap { - banner.format = [new Format(w: widthVal, h: heightVal)] + banner.format = [new Format(weight: widthVal, height: heightVal)] } } org.prebid.server.functional.model.response.auction.MediaType.BANNER | { int widthVal, int heightVal -> Imp.getDefaultImpression(mediaType).tap { banner.format = null - banner.w = widthVal - banner.h = heightVal + banner.weight = widthVal + banner.height = heightVal } } org.prebid.server.functional.model.response.auction.MediaType.VIDEO | { int widthVal, int heightVal -> Imp.getDefaultImpression(mediaType).tap { - video.w = widthVal - video.h = heightVal + video.weight = widthVal + video.height = heightVal } } } @@ -405,32 +412,38 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { BidRequest.getDefaultBidRequest(distributionChannel).tap { site.domain = publisherDomain site.publisher.id = publisherAccountId - } } + } + } SITE | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { site.publisher.domain = publisherDomain site.publisher.id = publisherAccountId - } } + } + } APP | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { app.domain = publisherDomain app.publisher.id = publisherAccountId - } } + } + } APP | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { app.publisher.domain = publisherDomain app.publisher.id = publisherAccountId - } } - DOOH | { String publisherDomain, String publisherAccountId -> + } + } + DOOH | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { dooh.domain = publisherDomain dooh.publisher.id = publisherAccountId - } } - DOOH | { String publisherDomain, String publisherAccountId -> + } + } + DOOH | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { dooh.publisher.domain = publisherDomain dooh.publisher.id = publisherAccountId - } } + } + } } def "PBS should choose correct rule when siteDomain is defined in rules for #distributionChannel channel"() { @@ -476,7 +489,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { app.publisher.id = publisherAccountId } } - DOOH | { String publisherDomain, String publisherAccountId -> + DOOH | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { dooh.domain = publisherDomain dooh.publisher.id = publisherAccountId @@ -527,7 +540,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { app.publisher.id = publisherAccountId } } - DOOH | { String publisherDomain, String publisherAccountId -> + DOOH | { String publisherDomain, String publisherAccountId -> BidRequest.getDefaultBidRequest(distributionChannel).tap { dooh.publisher.domain = publisherDomain dooh.publisher.id = publisherAccountId @@ -923,7 +936,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Set bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -941,7 +954,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_PRICE_FLOOR + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() where: @@ -970,7 +983,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(pbsService, bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Set bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -988,4 +1001,152 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { where: enforcePbs << [true, null] } + + def "PBS should use correct rule for each bidder when a bidder is defined with valid bid floor value"() { + given: "PBS config with openX bidder" + def floorsPbsService = pbsServiceFactory.getService( + FLOORS_CONFIG + GENERIC_ALIAS_CONFIG + + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + + and: "Default bid Request with generic and openx bidder within separate imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + addImp(Imp.defaultImpression.tap { + ext.prebid.bidder.generic = null + ext.prebid.bidder.openx = Openx.defaultOpenx + }) + } + + and: "Account with enabled fetch, fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [BIDDER]) + modelGroups[0].values = [(new Rule(bidder: GENERIC).rule): genericBidFloorRuleValue, + (new Rule(bidder: OPENX).rule) : openxBidFloorRuleValue] + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to appropriate rule" + def bidderRequest = getRequests(response) + assert bidderRequest.size() == 2 + assert bidderRequest[GENERIC.value].first.imp[0].bidFloor == genericBidFloorRuleValue + assert bidderRequest[OPENX.value].first.imp[0].bidFloor == openxBidFloorRuleValue + + and: "Bidder request should contain proper bid floor value" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + def impIdToBidderCallImp = impIdToBidderCallImp(bidderRequests) + assert impIdToBidderCallImp[bidRequest.imp[0].id].bidFloor == genericBidFloorRuleValue + assert impIdToBidderCallImp[bidRequest.imp[1].id].bidFloor == openxBidFloorRuleValue + + where: + genericBidFloorRuleValue | openxBidFloorRuleValue + null | PBSUtils.randomFloorValue + PBSUtils.randomFloorValue | null + PBSUtils.randomFloorValue | PBSUtils.randomFloorValue + } + + def "PBS shouldn't apply the floor rule for the main bidder to alias bidder"() { + given: "Default bid request with generic and alias bidder within separate imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.aliases = [(ALIAS.value): GENERIC] + addImp(Imp.defaultImpression.tap { + ext.prebid.bidder.generic = null + ext.prebid.bidder.alias = new Generic() + }) + } + + and: "Account with enabled fetch, fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [BIDDER]) + modelGroups[0].values = [(new Rule(bidder: GENERIC).rule): genericBidFloorRuleValue] + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to appropriate rule" + def bidderRequest = getRequests(response) + assert bidderRequest.size() == 2 + assert bidderRequest[GENERIC.value].first.imp[0].bidFloor == genericBidFloorRuleValue + assert !bidderRequest[ALIAS.value].first.imp[0].bidFloor + + and: "Bidder request should contain proper bid floor value" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + def impIdToBidderCallImp = impIdToBidderCallImp(bidderRequests) + assert impIdToBidderCallImp[bidRequest.imp[0].id].bidFloor == genericBidFloorRuleValue + assert !impIdToBidderCallImp[bidRequest.imp[1].id].bidFloor + + where: + genericBidFloorRuleValue | aliasBidFloorRuleValue + null | PBSUtils.randomFloorValue + PBSUtils.randomFloorValue | null + PBSUtils.randomFloorValue | PBSUtils.randomFloorValue + } + + def "PBS shouldn't apply the floor rule for the alias bidder to main bidder"() { + given: "Default bid request with generic and alias bidder within separate imp" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.aliases = [(ALIAS.value): GENERIC] + addImp(Imp.defaultImpression.tap { + ext.prebid.bidder.generic = null + ext.prebid.bidder.alias = new Generic() + }) + } + + and: "Account with enabled fetch, fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [BIDDER]) + modelGroups[0].values = [(new Rule(bidder: ALIAS).rule): aliasBidFloorRuleValue] + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to appropriate rule" + def bidderRequest = getRequests(response) + assert bidderRequest.size() == 2 + assert bidderRequest[ALIAS.value].first.imp[0].bidFloor == aliasBidFloorRuleValue + assert !bidderRequest[GENERIC.value].first.imp[0].bidFloor + + and: "Bidder request should contain proper bid floor value" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + def impIdToBidderCallImp = impIdToBidderCallImp(bidderRequests) + assert impIdToBidderCallImp[bidRequest.imp[1].id].bidFloor == aliasBidFloorRuleValue + assert !impIdToBidderCallImp[bidRequest.imp[0].id].bidFloor + + where: + genericBidFloorRuleValue | aliasBidFloorRuleValue + null | PBSUtils.randomFloorValue + PBSUtils.randomFloorValue | null + PBSUtils.randomFloorValue | PBSUtils.randomFloorValue + } + + private static Map impIdToBidderCallImp(List bidderRequests) { + bidderRequests.imp.flatten().collectEntries { [it.id, it] } as Map + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy index c5eea7563aa..737aed289e4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy @@ -99,7 +99,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(bidRequest, floorsProviderFloorValue) + cacheFloorsProviderRules(bidRequest, floorsProviderFloorValue, floorsPbsService) then: "Bidder request bidFloor should correspond to floors provider" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -349,7 +349,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(pbsService, bidRequest, floorsProviderFloorValue) + cacheFloorsProviderRules(bidRequest, floorsProviderFloorValue / bidAdjustment, pbsService) then: "Bidder request bidFloor should be update according to bidAdjustment" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -369,7 +369,10 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def "PBS should not update imp[0].bidFloor when bidadjustment is disallowed"() { given: "Pbs with PF configuration with adjustForBidAdjustment" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { - auction.priceFloors.adjustForBidAdjustment = pbsConfigBidAdjustmentFlag + auction.priceFloors.tap { + adjustForBidAdjustment = pbsConfigBidAdjustmentFlag + adjustForBidAdjustmentSnakeCase = pbsConfigBidAdjustmentFlagSnakeCase + } } def pbsService = pbsServiceFactory.getService(FLOORS_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]) @@ -386,6 +389,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def accountId = bidRequest.app.publisher.id def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.adjustForBidAdjustment = accountBidAdjustmentFlag + config.auction.priceFloors.adjustForBidAdjustmentSnakeCase = accountBidAdjustmentFlagSnakeCase } accountDao.save(account) @@ -396,7 +400,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(pbsService, bidRequest, floorsProviderFloorValue) + cacheFloorsProviderRules(bidRequest, floorsProviderFloorValue, pbsService) then: "Bidder request bidFloor should be changed" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -405,9 +409,11 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert bidderRequest.imp[0].ext.prebid.floors.floorValue == floorsProviderFloorValue where: - pbsConfigBidAdjustmentFlag | requestBidAdjustmentFlag | accountBidAdjustmentFlag - false | false | null - true | null | false + pbsConfigBidAdjustmentFlagSnakeCase | pbsConfigBidAdjustmentFlag | requestBidAdjustmentFlag | accountBidAdjustmentFlag | accountBidAdjustmentFlagSnakeCase + null | false | false | null | false + null | true | null | false | null + false | null | false | null | false + true | null | null | false | null } def "PBS should choose most aggressive adjustment when request contains multiple media-types"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy index 09d8c51b0e1..309bba2eea6 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy @@ -9,19 +9,20 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ActivityInfrastructure import org.prebid.server.functional.model.response.auction.ActivityInvocationPayload import org.prebid.server.functional.model.response.auction.And import org.prebid.server.functional.model.response.auction.GeoCode import org.prebid.server.functional.model.response.auction.RuleConfiguration import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.request.GppSectionId.USP_CA_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_TID @@ -93,7 +94,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert fetchBidsActivity.ruleConfiguration.every { it == null } assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) assert fetchBidsActivity.result.contains("DISALLOW") @@ -133,7 +134,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) assert fetchBidsActivity.ruleConfiguration.every { it == null } assert fetchBidsActivity.result.contains("ALLOW") @@ -148,7 +149,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitUfpdActivity.allowByDefault.contains(activity.defaultAction) assert transmitUfpdActivity.ruleConfiguration.every { it == null } assert transmitUfpdActivity.result.every { it == null } @@ -163,7 +164,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitPreciseGeoActivity.allowByDefault.contains(activity.defaultAction) assert transmitPreciseGeoActivity.ruleConfiguration.every { it == null } assert transmitPreciseGeoActivity.result.every { it == null } @@ -178,7 +179,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitTidActivity.allowByDefault.contains(activity.defaultAction) assert transmitTidActivity.ruleConfiguration.every { it == null } assert transmitTidActivity.result.every { it == null } @@ -192,15 +193,15 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE device = new Device(geo: new Geo(country: USA, region: ALABAMA.abbreviation)) - regs.ext.gpc = PBSUtils.randomString - regs.gppSid = [USP_CA_V1.intValue] + regs.ext = new RegsExt(gpc: PBSUtils.randomString) + regs.gppSid = [US_CA_V1.intValue] setAccountId(accountId) } and: "Set up activities" def gpc = PBSUtils.randomString def condition = Condition.baseCondition.tap { - it.gppSid = [USP_CO_V1.intValue] + it.gppSid = [US_CO_V1.intValue] it.gpc = gpc it.geo = [CAN.withState(ARIZONA)] } @@ -227,7 +228,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentType: BIDDER, gpc: bidRequest.regs.ext.gpc, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert fetchBidsActivity.ruleConfiguration.contains(new RuleConfiguration( componentNames: condition.componentName, componentTypes: condition.componentType, @@ -265,7 +266,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentType: BIDDER, gpc: bidRequest.regs.ext.gpc, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitPreciseGeoActivity.ruleConfiguration.every { it == null } assert transmitPreciseGeoActivity.allowByDefault.contains(activity.defaultAction) assert transmitPreciseGeoActivity.result.every { it == null } @@ -298,9 +299,9 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE device = new Device(geo: new Geo(country: USA, region: ALABAMA.abbreviation)) - regs.ext.gpc = PBSUtils.randomString - regs.gppSid = [USP_CA_V1.intValue] - regs.gpp = new UspNatV1Consent.Builder().setGpc(true).build() + regs.ext = new RegsExt(gpc: PBSUtils.randomString) + regs.gppSid = [US_CA_V1.intValue] + regs.gpp = new UsNatV1Consent.Builder().setGpc(true).build() setAccountId(accountId) } @@ -404,4 +405,3 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { activityInfrastructures[new IntRange(true, firstIndex, lastIndex)] } } - diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy index fc1436a9ee9..945ee175ee1 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAmpSpec.groovy @@ -14,8 +14,14 @@ import spock.lang.PendingFeature import static org.prebid.server.functional.model.ChannelType.AMP import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS import static org.prebid.server.functional.model.request.amp.ConsentType.TCF_1 +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS @@ -100,7 +106,9 @@ class CcpaAmpSpec extends PrivacyBaseSpec { def ampRequest = getCcpaAmpRequest(validCcpa) and: "Save storedRequest into DB" - def ampStoredRequest = storedRequestWithGeo + def ampStoredRequest = storedRequestWithGeo.tap { + ext.prebid.trace = VERBOSE + } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -110,15 +118,28 @@ class CcpaAmpSpec extends PrivacyBaseSpec { def account = new Account(uuid: ampRequest.account, config: accountConfig) accountDao.save(account) + and: "Flush metrics" + flushMetrics(privacyPbsService) + when: "PBS processes amp request" - defaultPbsService.sendAmpRequest(ampRequest) + privacyPbsService.sendAmpRequest(ampRequest) then: "Bidder request should contain masked values" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) assert bidderRequests.device?.geo == maskGeo(ampStoredRequest) + and: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_PRECISE_GEO)] == 1 + where: ccpaConfig << [new AccountCcpaConfig(enabled: false, channelEnabled: [(AMP): true]), + new AccountCcpaConfig(enabled: false, channelEnabledSnakeCase: [(AMP): true]), new AccountCcpaConfig(enabled: true)] } @@ -138,14 +159,26 @@ class CcpaAmpSpec extends PrivacyBaseSpec { def account = new Account(uuid: ampRequest.account, config: accountConfig) accountDao.save(account) + and: "Flush metrics" + flushMetrics(privacyPbsService) + when: "PBS processes amp request" - defaultPbsService.sendAmpRequest(ampRequest) + privacyPbsService.sendAmpRequest(ampRequest) then: "Bidder request should contain not masked values" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) assert bidderRequests.device?.geo?.lat == ampStoredRequest.device.geo.lat assert bidderRequests.device?.geo?.lon == ampStoredRequest.device.geo.lon + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, ampRequest.account, TRANSMIT_PRECISE_GEO)] + where: ccpaConfig << [new AccountCcpaConfig(enabled: true, channelEnabled: [(AMP): false]), new AccountCcpaConfig(enabled: false)] diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy index b53e837b6ec..d544c7a2788 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/CcpaAuctionSpec.groovy @@ -10,7 +10,15 @@ import spock.lang.PendingFeature import static org.prebid.server.functional.model.ChannelType.PBJS import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.Prebid.Channel +import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED class CcpaAuctionSpec extends PrivacyBaseSpec { @@ -115,21 +123,77 @@ class CcpaAuctionSpec extends PrivacyBaseSpec { } } - def "PBS should apply ccpa when privacy.ccpa.channel-enabled.app or privacy.ccpa.enabled = true in account config"() { + def "PBS should apply ccpa and emit full metrics when privacy.ccpa.channel-enabled.app or privacy.ccpa.enabled = true in account config and trace level verbose"() { given: "Default basic generic BidRequest" def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - def bidRequest = getCcpaBidRequest(DistributionChannel.APP, validCcpa) + def bidRequest = getCcpaBidRequest(DistributionChannel.APP, validCcpa).tap { + ext.prebid.trace = VERBOSE + } and: "Save account config into DB" - accountDao.save(getAccountWithCcpa(bidRequest.app.publisher.id, ccpaConfig)) + accountDao.save(getAccountWithCcpa(bidRequest.accountId, ccpaConfig)) + + and: "Flush metrics" + flushMetrics(privacyPbsService) when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) + privacyPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain masked values" def bidderRequests = bidder.getBidderRequest(bidRequest.id) assert bidderRequests.device?.geo == maskGeo(bidRequest) + and: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + where: + ccpaConfig << [new AccountCcpaConfig(enabled: false, channelEnabled: [(ChannelType.APP): true]), + new AccountCcpaConfig(enabled: true)] + } + + def "PBS should apply ccpa and emit metrics when privacy.ccpa.channel-enabled.app or privacy.ccpa.enabled = true in account config and trace level basic"() { + given: "Default basic generic BidRequest" + def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) + def bidRequest = getCcpaBidRequest(DistributionChannel.APP, validCcpa).tap { + ext.prebid.trace = BASIC + } + + and: "Save account config into DB" + accountDao.save(getAccountWithCcpa(bidRequest.accountId, ccpaConfig)) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain masked values" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device?.geo == maskGeo(bidRequest) + + and: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + and: "Metrics account shouldn't be populated" + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + where: ccpaConfig << [new AccountCcpaConfig(enabled: false, channelEnabled: [(ChannelType.APP): true]), new AccountCcpaConfig(enabled: true)] @@ -171,6 +235,18 @@ class CcpaAuctionSpec extends PrivacyBaseSpec { assert bidderRequests.device?.geo?.lat == bidRequest.device.geo.lat assert bidderRequests.device?.geo?.lon == bidRequest.device.geo.lon + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + where: ccpaConfig << [new AccountCcpaConfig(enabled: true, channelEnabled: [(ChannelType.APP): false]), new AccountCcpaConfig(enabled: false)] diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy index dac9fafca18..134aa10c9a4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/CoppaSpec.groovy @@ -5,6 +5,14 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import spock.lang.PendingFeature import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD +import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE class CoppaSpec extends PrivacyBaseSpec { @@ -139,4 +147,339 @@ class CoppaSpec extends PrivacyBaseSpec { privacy.errors?.isEmpty() } } + + def "PBS shouldn't mask device and user fields for auction request when coppa = 0 was passed"() { + given: "BidRequest with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.coppa = 0 + } + + and: "FLush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.device.ip == bidRequest.device.ip + bidderRequest.device.ipv6 == "af47:892b:3e98:b49a::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat + bidderRequest.device.geo.lon == bidRequest.device.geo.lon + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + bidderRequest.device.geo.metro == bidRequest.device.geo.metro + bidderRequest.device.geo.city == bidRequest.device.geo.city + bidderRequest.device.geo.zip == bidRequest.device.geo.zip + bidderRequest.device.geo.accuracy == bidRequest.device.geo.accuracy + bidderRequest.device.geo.ipservice == bidRequest.device.geo.ipservice + bidderRequest.device.geo.ext == bidRequest.device.geo.ext + + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo.lat == bidRequest.user.geo.lat + bidderRequest.user.geo.lon == bidRequest.user.geo.lon + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid + } + + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS should mask device and user fields for auction request when coppa = 1 was passed and trace level verbose"() { + given: "BidRequest with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.coppa = 1 + ext.prebid.trace = VERBOSE + } + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + } + + def "PBS should mask device and user fields for auction request when coppa = 1 was passed and trace level basic"() { + given: "BidRequest with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.coppa = 1 + ext.prebid.trace = BASIC + } + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + and: "Account metrics shouldn't be updated" + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS shouldn't mask device and user fields for amp request when coppa = 0 was passed"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Save storedRequest into DB" + def ampStoredRequest = bidRequestWithPersonalData.tap { + regs.coppa = 0 + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request shouldn't mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.device.ip == ampStoredRequest.device.ip + bidderRequest.device.ipv6 == "af47:892b:3e98:b49a::" + bidderRequest.device.geo.lat == ampStoredRequest.device.geo.lat + bidderRequest.device.geo.lon == ampStoredRequest.device.geo.lon + bidderRequest.device.geo.country == ampStoredRequest.device.geo.country + bidderRequest.device.geo.region == ampStoredRequest.device.geo.region + bidderRequest.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + bidderRequest.device.geo.metro == ampStoredRequest.device.geo.metro + bidderRequest.device.geo.city == ampStoredRequest.device.geo.city + bidderRequest.device.geo.zip == ampStoredRequest.device.geo.zip + bidderRequest.device.geo.accuracy == ampStoredRequest.device.geo.accuracy + bidderRequest.device.geo.ipservice == ampStoredRequest.device.geo.ipservice + bidderRequest.device.geo.ext == ampStoredRequest.device.geo.ext + + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequest.user.geo.lon == ampStoredRequest.user.geo.lon + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS should mask device and user fields for amp request when coppa = 1 was passed"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Save storedRequest into DB" + def ampStoredRequest = bidRequestWithPersonalData.tap { + regs.coppa = 1 + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == ampStoredRequest.device.geo.country + bidderRequest.device.geo.region == ampStoredRequest.device.geo.region + bidderRequest.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy index e7576c23396..2575789049d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy @@ -6,14 +6,23 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Dsa import org.prebid.server.functional.model.request.auction.Dsa as RequestDsa -import org.prebid.server.functional.model.request.auction.DsaRequired +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.Dsa as BidDsa +import org.prebid.server.functional.model.response.auction.DsaResponse +import org.prebid.server.functional.model.response.auction.DsaResponse as BidDsa import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.TcfConsent -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.GENERAL +import static org.prebid.server.functional.model.request.auction.DsaPubRender.PUB_CANT_RENDER +import static org.prebid.server.functional.model.request.auction.DsaPubRender.PUB_WILL_RENDER +import static org.prebid.server.functional.model.request.auction.DsaRequired.NOT_REQUIRED +import static org.prebid.server.functional.model.request.auction.DsaRequired.REQUIRED +import static org.prebid.server.functional.model.request.auction.DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM +import static org.prebid.server.functional.model.request.auction.DsaRequired.SUPPORTED +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_DSA +import static org.prebid.server.functional.model.response.auction.DsaAdRender.ADVERTISER_WILL_RENDER +import static org.prebid.server.functional.model.response.auction.DsaAdRender.ADVERTISER_WONT_RENDER import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS @@ -26,7 +35,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -43,10 +52,10 @@ class DsaSpec extends PrivacyBaseSpec { where: dsa << [null, new RequestDsa(), - RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + RequestDsa.getDefaultDsa(NOT_REQUIRED), + RequestDsa.getDefaultDsa(SUPPORTED), + RequestDsa.getDefaultDsa(REQUIRED), + RequestDsa.getDefaultDsa(REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] } def "AMP request should always accept bids with DSA"() { @@ -55,7 +64,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -85,10 +94,10 @@ class DsaSpec extends PrivacyBaseSpec { where: dsa << [null, new RequestDsa(), - RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + RequestDsa.getDefaultDsa(NOT_REQUIRED), + RequestDsa.getDefaultDsa(SUPPORTED), + RequestDsa.getDefaultDsa(REQUIRED), + RequestDsa.getDefaultDsa(REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] } def "AMP request should accept bids without DSA when dsarequired is #dsaRequired"() { @@ -98,7 +107,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -125,8 +134,7 @@ class DsaSpec extends PrivacyBaseSpec { assert !response.ext.errors where: - dsaRequired << [DsaRequired.NOT_REQUIRED, - DsaRequired.SUPPORTED] + dsaRequired << [NOT_REQUIRED, SUPPORTED] } def "AMP request should reject bids without DSA when dsarequired is #dsaRequired"() { @@ -136,7 +144,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored bid request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -161,17 +169,16 @@ class DsaSpec extends PrivacyBaseSpec { and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id assert response.ext?.warnings[GENERIC]*.code == [5] - assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\" missing DSA"] + assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\": DSA object missing when required"] where: - dsaRequired << [DsaRequired.REQUIRED, - DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] + dsaRequired << [REQUIRED, REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] } def "Auction request should always forward DSA to bidders"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) } when: "PBS processes auction request" @@ -183,16 +190,16 @@ class DsaSpec extends PrivacyBaseSpec { where: dsa << [null, new RequestDsa(), - RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + RequestDsa.getDefaultDsa(NOT_REQUIRED), + RequestDsa.getDefaultDsa(SUPPORTED), + RequestDsa.getDefaultDsa(REQUIRED), + RequestDsa.getDefaultDsa(REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] } def "Auction request should always accept bids with DSA"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) } and: "Default bidder response with DSA" @@ -220,16 +227,16 @@ class DsaSpec extends PrivacyBaseSpec { where: dsa << [null, new RequestDsa(), - RequestDsa.getDefaultDsa(DsaRequired.NOT_REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.SUPPORTED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED), - RequestDsa.getDefaultDsa(DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] + RequestDsa.getDefaultDsa(NOT_REQUIRED), + RequestDsa.getDefaultDsa(SUPPORTED), + RequestDsa.getDefaultDsa(REQUIRED), + RequestDsa.getDefaultDsa(REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM)] } def "Auction request should accept bids without DSA when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response with DSA" @@ -251,14 +258,13 @@ class DsaSpec extends PrivacyBaseSpec { assert !response.ext.errors where: - dsaRequired << [DsaRequired.NOT_REQUIRED, - DsaRequired.SUPPORTED] + dsaRequired << [NOT_REQUIRED, SUPPORTED] } def "Auction request should reject bids without DSA when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response without DSA" @@ -278,18 +284,17 @@ class DsaSpec extends PrivacyBaseSpec { and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id assert response.ext?.warnings[GENERIC]*.code == [5] - assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\" missing DSA"] + assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\": DSA object missing when required"] where: - dsaRequired << [DsaRequired.REQUIRED, - DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] + dsaRequired << [REQUIRED, REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] } def "Auction request should reject bids without DSA and populate seatNonBid when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response without DSA" @@ -312,16 +317,15 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == GENERAL + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id assert response.ext?.warnings[GENERIC]*.code == [5] - assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\" missing DSA"] + assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\": DSA object missing when required"] where: - dsaRequired << [DsaRequired.REQUIRED, - DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] + dsaRequired << [REQUIRED, REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM] } def "Auction request should set account DSA when BidRequest DSA is null"() { @@ -329,7 +333,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account with default DSA config" @@ -349,7 +353,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = requestDsa + regs.ext = new RegsExt(dsa: requestDsa) } and: "Account with default DSA config" @@ -375,7 +379,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account without default DSA config" @@ -394,8 +398,8 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null - regs.ext.gdpr = 0 + regs.ext = new RegsExt(dsa: null) + regs.gdpr = 0 } and: "Account with default DSA config" @@ -421,7 +425,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = getGdprBidRequest(consentString).tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account with default DSA config" @@ -445,14 +449,12 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null - regs.ext.gdpr = 0 + regs.ext = new RegsExt(dsa: null) + regs.gdpr = 0 } and: "Account with default DSA config" - def accountDsa = Dsa.defaultDsa - def account = getAccountWithDsa(accountId, - new AccountDsaConfig(defaultDsa: accountDsa, gdprOnly: true)) + def account = getAccountWithDsa(accountId, accountDsaConfig) accountDao.save(account) when: "PBS processes auction request" @@ -460,5 +462,91 @@ class DsaSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain DSA" assert !bidder.getBidderRequest(bidRequest.id)?.regs?.ext?.dsa + + where: + accountDsaConfig << [new AccountDsaConfig(defaultDsa: Dsa.defaultDsa, gdprOnly: true), + new AccountDsaConfig(defaultDsa: Dsa.defaultDsa, gdprOnlySnakeCase: true)] + } + + def "Auction request should reject bids with DSA when pubRender is #pubRender and adRender is #adRender"() { + given: "Default bid request with DSA pubRender" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(REQUIRED).tap { + it.pubRender = pubRender + }) + } + + and: "Default bidder response with incorrect DSA adRender" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: new DsaResponse(adRender: adRender)) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject bid" + assert !response.seatbid + + and: "PBS response should contain seatNonBid for rejected bids" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA + + and: "Response should contain an error" + def bidId = bidResponse.seatbid[0].bid[0].id + assert response.ext?.warnings[GENERIC]*.code == [5] + assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\": ${warningMessage}"] + + where: + warningMessage | pubRender | adRender + "DSA publisher and buyer both signal will render" | PUB_WILL_RENDER | ADVERTISER_WILL_RENDER + "DSA publisher and buyer both signal will not render" | PUB_CANT_RENDER | ADVERTISER_WONT_RENDER + } + + def "Auction request should reject bids with DSA when dsa response have paid or behalf fields longer then 100 characters"() { + given: "Default bid request with DSA pubRender" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(REQUIRED)) + } + + and: "Default bidder response with incorrect DSA" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].ext = new BidExt(dsa: invalidDsaResponse) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject bid" + assert !response.seatbid + + and: "PBS response should contain seatNonBid for rejected bids" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA + + and: "Response should contain an error" + def bidId = bidResponse.seatbid[0].bid[0].id + assert response.ext?.warnings[GENERIC]*.code == [5] + assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\": ${warningMessage}"] + + where: + warningMessage | invalidDsaResponse + "DSA paid exceeds limit of 100 chars" | new DsaResponse(paid: PBSUtils.getRandomString(101)) + "DSA behalf exceeds limit of 100 chars" | new DsaResponse(behalf: PBSUtils.getRandomString(101)) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy index d9df6ab076e..d4cfc7bbda9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy @@ -1,9 +1,11 @@ package org.prebid.server.functional.tests.privacy +import org.mockserver.model.Delay import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.auction.BidRequest @@ -19,15 +21,29 @@ import spock.lang.PendingFeature import java.time.Instant import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.Purpose.P1 +import static org.prebid.server.functional.model.config.Purpose.P2 +import static org.prebid.server.functional.model.config.Purpose.P4 +import static org.prebid.server.functional.model.config.PurposeEnforcement.BASIC +import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS import static org.prebid.server.functional.model.request.amp.ConsentType.TCF_1 import static org.prebid.server.functional.model.request.amp.ConsentType.US_PRIVACY +import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 -import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V3 +import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V4 +import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V5 class GdprAmpSpec extends PrivacyBaseSpec { @@ -257,6 +273,7 @@ class GdprAmpSpec extends PrivacyBaseSpec { where: gdprConfig << [new AccountGdprConfig(enabled: false, channelEnabled: [(ChannelType.AMP): true]), + new AccountGdprConfig(enabled: false, channelEnabledSnakeCase: [(ChannelType.AMP): true]), new AccountGdprConfig(enabled: true)] } @@ -327,7 +344,7 @@ class GdprAmpSpec extends PrivacyBaseSpec { privacyPbsService.sendAmpRequest(ampRequest) then: "Used vendor list have proper specification version of GVL" - def properVendorListPath = "/app/prebid-server/data/vendorlist-v${tcfPolicyVersion.vendorListVersion}/${tcfPolicyVersion.vendorListVersion}.json" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion @@ -341,14 +358,15 @@ class GdprAmpSpec extends PrivacyBaseSpec { serverContainer.stop() where: - tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V3] + tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } - def "PBS amp with invalid consent.tcfPolicyVersion parameter should reject request and include proper warning"() { - given: "Tcf consent string" - def invalidTcfPolicyVersion = PBSUtils.getRandomNumber(5, 63) + def "PBS amp shouldn't reject request with proper warning and metrics when incoming consent.tcfPolicyVersion have invalid parameter"() { + given: "Tcf consent string with invalid tcf policy version" def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) .setTcfPolicyVersion(invalidTcfPolicyVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() and: "AMP request" @@ -361,12 +379,311 @@ class GdprAmpSpec extends PrivacyBaseSpec { def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) + and: "Flush metrics" + flushMetrics(privacyPbsService) + when: "PBS processes amp request" def response = privacyPbsService.sendAmpRequest(ampRequest) then: "Bid response should contain warning" assert response.ext?.warnings[PREBID]*.code == [999] assert response.ext?.warnings[PREBID]*.message == - ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] + ["Unknown tcfPolicyVersion ${invalidTcfPolicyVersion}, defaulting to gvlSpecificationVersion=3" as String] + + and: "Alerts.general metrics should be populated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["alerts.general"] == 1 + + and: "Bidder should be called" + assert bidder.getBidderRequest(ampStoredRequest.id) + + where: + invalidTcfPolicyVersion << [MIN_INVALID_TCF_POLICY_VERSION, + PBSUtils.getRandomNumber(MIN_INVALID_TCF_POLICY_VERSION, MAX_INVALID_TCF_POLICY_VERSION), + MAX_INVALID_TCF_POLICY_VERSION] + } + + def "PBS amp should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Prepare tcf consent string" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "AMP request" + def ampRequest = getGdprAmpRequest(tcfConsent) + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Reset valid vendor list response" + vendorListResponse.reset() + + and: "Set vendor list response with delay" + vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3)) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS shouldn't fetch vendor list" + def vendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + assert !privacyPbsService.isFileExist(vendorListPath) + + and: "Logs should contain proper vendor list version" + def logs = privacyPbsService.getLogsByTime(startTime) + def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading." + assert getLogsByText(logs, tcfError) + + and: "Second start for fetch second round of logs" + def secondStartTime = Instant.now() + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS shouldn't fetch vendor list" + assert !privacyPbsService.isFileExist(vendorListPath) + + and: "Logs should contain proper vendor list version" + def logsSecond = privacyPbsService.getLogsByTime(secondStartTime) + assert getLogsByText(logsSecond, tcfError) + + and: "Reset vendor list response" + vendorListResponse.reset() + + where: + tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] + } + + def "PBS amp should update activity controls fetch bids metrics when tcf requirement disallow request"() { + given: "Default ampStoredRequests with personal data" + def ampStoredRequest = bidRequestWithPersonalData + + and: "Amp default request" + def tcfConsent = new TcfConsent.Builder().build() + def ampRequest = getGdprAmpRequest(tcfConsent).tap { + account = ampStoredRequest.accountId + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: BASIC, enforceVendors: true)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(ampStoredRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS should cansel request" + assert !bidder.getBidderRequests(ampStoredRequest.id) + + then: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + } + + def "PBS auction should update activity controls privacy metrics when tcf requirement disallow privacy fields"() { + given: "Default ampStoredRequests with personal data" + def ampStoredRequest = bidRequestWithPersonalData + + and: "Amp default request" + def tcfConsent = new TcfConsent.Builder().build() + def ampRequest = getGdprAmpRequest(tcfConsent).tap { + account = ampStoredRequest.accountId + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(ampStoredRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == ampStoredRequest.device.geo.country + bidderRequest.device.geo.region == ampStoredRequest.device.geo.region + bidderRequest.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + } + + def "PBS auction should not update activity controls privacy metrics when tcf requirement allow privacy fields"() { + given: "Default ampStoredRequests with personal data" + def ampStoredRequest = bidRequestWithPersonalData + + and: "Amp default request" + def tcfConsent = new TcfConsent.Builder().setSpecialFeatureOptIns(DEVICE_ACCESS).build() + def ampRequest = getGdprAmpRequest(tcfConsent).tap { + account = ampStoredRequest.accountId + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P1): new PurposeConfig(enforcePurpose: NO, enforceVendors: false), + (P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false), + (P4): new PurposeConfig(enforcePurpose: NO, enforceVendors: false), + ] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(ampStoredRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request shouldn't mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.device.ip == ampStoredRequest.device.ip + bidderRequest.device.ipv6 == "af47:892b:3e98:b49a::" + bidderRequest.device.geo.lat == ampStoredRequest.device.geo.lat + bidderRequest.device.geo.lon == ampStoredRequest.device.geo.lon + bidderRequest.device.geo.country == ampStoredRequest.device.geo.country + bidderRequest.device.geo.region == ampStoredRequest.device.geo.region + bidderRequest.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + bidderRequest.device.geo.metro == ampStoredRequest.device.geo.metro + bidderRequest.device.geo.city == ampStoredRequest.device.geo.city + bidderRequest.device.geo.zip == ampStoredRequest.device.geo.zip + bidderRequest.device.geo.accuracy == ampStoredRequest.device.geo.accuracy + bidderRequest.device.geo.ipservice == ampStoredRequest.device.geo.ipservice + bidderRequest.device.geo.ext == ampStoredRequest.device.geo.ext + + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequest.user.geo.lon == ampStoredRequest.user.geo.lon + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS amp should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() { + given: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "AMP request" + def ampRequest = getGdprAmpRequest(tcfConsent) + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } + def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 3d3ad19505c..c4a21342ab9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -1,7 +1,10 @@ package org.prebid.server.functional.tests.privacy +import org.mockserver.model.Delay import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountGdprConfig +import org.prebid.server.functional.model.config.PurposeConfig +import org.prebid.server.functional.model.config.PurposeEnforcement import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService @@ -17,12 +20,31 @@ import java.time.Instant import static org.prebid.server.functional.model.ChannelType.PBJS import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.Purpose.P1 +import static org.prebid.server.functional.model.config.Purpose.P2 +import static org.prebid.server.functional.model.config.Purpose.P4 +import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA +import static org.prebid.server.functional.model.pricefloors.Country.CAN +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT +import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.Prebid.Channel -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_PRIVACY +import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_PRIVACY import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 -import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V3 +import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V4 +import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V5 class GdprAuctionSpec extends PrivacyBaseSpec { @@ -241,7 +263,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_BY_PRIVACY + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_PRIVACY and: "seatbid should be empty" assert response.seatbid.isEmpty() @@ -275,7 +297,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { privacyPbsService.sendAuctionRequest(bidRequest) then: "Used vendor list have proper specification version of GVL" - def properVendorListPath = "/app/prebid-server/data/vendorlist-v${tcfPolicyVersion.vendorListVersion}/${tcfPolicyVersion.vendorListVersion}.json" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion @@ -289,14 +311,15 @@ class GdprAuctionSpec extends PrivacyBaseSpec { serverContainer.stop() where: - tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V3] + tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } - def "PBS auction should reject request with proper warning when incoming consent.tcfPolicyVersion have invalid parameter"() { - given: "Tcf consent string" - def invalidTcfPolicyVersion = PBSUtils.getRandomNumber(5, 63) + def "PBS auction shouldn't reject request with proper warning and metrics when incoming consent.tcfPolicyVersion have invalid parameter"() { + given: "Tcf consent string with invalid tcf policy version" def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) .setTcfPolicyVersion(invalidTcfPolicyVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() and: "Bid request" @@ -311,6 +334,475 @@ class GdprAuctionSpec extends PrivacyBaseSpec { then: "Bid response should contain warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] assert response.ext?.warnings[ErrorType.PREBID]*.message == - ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] + ["Unknown tcfPolicyVersion ${invalidTcfPolicyVersion}, defaulting to gvlSpecificationVersion=3" as String] + + and: "Alerts.general metrics should be populated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["alerts.general"] == 1 + + and: "Bid response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Bidder should be called" + assert bidder.getBidderRequest(bidRequest.id) + + where: + invalidTcfPolicyVersion << [MIN_INVALID_TCF_POLICY_VERSION, + PBSUtils.getRandomNumber(MIN_INVALID_TCF_POLICY_VERSION, MAX_INVALID_TCF_POLICY_VERSION), + MAX_INVALID_TCF_POLICY_VERSION] + } + + def "PBS auction should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Reset valid vendor list response" + vendorListResponse.reset() + + and: "Set vendor list response with delay" + vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3)) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + assert !privacyPbsService.isFileExist(properVendorListPath) + + and: "Logs should contain proper vendor list version" + def logs = privacyPbsService.getLogsByTime(startTime) + def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading." + assert getLogsByText(logs, tcfError) + + and: "Second start for fetch second round of logs" + def secondStartTime = Instant.now() + + when: "PBS processes amp request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't fetch vendor list" + assert !privacyPbsService.isFileExist(properVendorListPath) + + and: "Logs should contain proper vendor list version" + def logsSecond = privacyPbsService.getLogsByTime(secondStartTime) + assert getLogsByText(logsSecond, tcfError) + + and: "Reset vendor list response" + vendorListResponse.reset() + + where: + tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] + } + + def "PBS should apply gdpr and emit metrics when host and device.geo.country contains same eea-country"() { + given: "Valid consent string" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Gpdr bid request with override country" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + device.geo.country = BULGARIA + } + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id, + new AccountGdprConfig(enabled: true, eeaCountries: null))) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBs should increment metrics when eea-country matched" + def metricsRequest = privacyPbsService.sendCollectedMetricsRequest() + assert metricsRequest["privacy.tcf.v2.in-geo"] == 1 + assert !metricsRequest["privacy.tcf.v2.out-geo"] + } + + def "PBS should apply gdpr and not emit metrics when host and device.geo.country doesn't contain same eea-country"() { + given: "Valid consent string" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Gpdr bid request with override country" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + device.geo.country = USA + } + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id, + new AccountGdprConfig(enabled: true, eeaCountries: null))) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBs should increment metrics when eea-country doens't matched" + def metricsRequest = privacyPbsService.sendCollectedMetricsRequest() + assert !metricsRequest["privacy.tcf.v2.in-geo"] + assert metricsRequest["privacy.tcf.v2.out-geo"] == 1 + } + + def "PBS should apply gdpr and emit metrics when account and device.geo.country contains same eea-country"() { + given: "Valid consent string" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Gpdr bid request with override country" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + device.geo.country = USA + } + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id, + new AccountGdprConfig(enabled: true, eeaCountries: USA.ISOAlpha2))) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBs should increment metrics when eea-country matched" + def metricsRequest = privacyPbsService.sendCollectedMetricsRequest() + assert metricsRequest["privacy.tcf.v2.in-geo"] == 1 + assert !metricsRequest["privacy.tcf.v2.out-geo"] + } + + def "PBS should apply gdpr and not emit metrics when account and device.geo.country doesn't contain same eea-country"() { + given: "Valid consent string" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Gpdr bid request with override country" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + device.geo.country = USA + } + + and: "Save account config into DB" + accountDao.save(getAccountWithGdpr(bidRequest.app.publisher.id, + new AccountGdprConfig(enabled: true, eeaCountries: CAN.ISOAlpha2))) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBs shouldn't increment metrics when eea-country matched" + def metricsRequest = privacyPbsService.sendCollectedMetricsRequest() + assert !metricsRequest["privacy.tcf.v2.in-geo"] + assert metricsRequest["privacy.tcf.v2.out-geo"] == 1 + } + + def "PBS auction should update activity controls fetch bids metrics when tcf requirement disallow request"() { + given: "Default Generic bid requests with personal data" + def tcfConsent = new TcfConsent.Builder().build() + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = tcfConsent + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: PurposeEnforcement.BASIC, enforceVendors: true)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should cansel request" + assert !bidder.getBidderRequests(bidRequest.id) + + then: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + } + + def "PBS auction should update activity controls privacy metrics when tcf requirement disallow privacy fields and trace level verbosity"() { + given: "Default Generic BidRequests with personal data" + def tcfConsent = new TcfConsent.Builder().build() + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = tcfConsent + ext.prebid.trace = VERBOSE + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + } + + def "PBS auction should update activity controls privacy metrics when tcf requirement disallow privacy fields and trace level basic"() { + given: "Default Generic BidRequests with personal data" + def tcfConsent = new TcfConsent.Builder().build() + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = tcfConsent + ext.prebid.trace = BASIC + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + and: "Account metrics shouldn't be updated" + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS auction should not update activity controls privacy metrics when tcf requirement allow privacy fields"() { + given: "Default Generic BidRequests with privacy data" + def tcfConsent = new TcfConsent.Builder().setSpecialFeatureOptIns(DEVICE_ACCESS).build() + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = tcfConsent + } + + new TcfConsent.Builder().setPurposesConsent([]).build().consentString + + and: "Save account config with requireConsent into DB" + def purposes = [(P1): new PurposeConfig(enforcePurpose: NO, enforceVendors: false), + (P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false), + (P4): new PurposeConfig(enforcePurpose: NO, enforceVendors: false), + ] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.device.ip == bidRequest.device.ip + bidderRequest.device.ipv6 == "af47:892b:3e98:b49a::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat + bidderRequest.device.geo.lon == bidRequest.device.geo.lon + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + bidderRequest.device.geo.metro == bidRequest.device.geo.metro + bidderRequest.device.geo.city == bidRequest.device.geo.city + bidderRequest.device.geo.zip == bidRequest.device.geo.zip + bidderRequest.device.geo.accuracy == bidRequest.device.geo.accuracy + bidderRequest.device.geo.ipservice == bidRequest.device.geo.ipservice + bidderRequest.device.geo.ext == bidRequest.device.geo.ext + + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo.lat == bidRequest.user.geo.lat + bidderRequest.user.geo.lon == bidRequest.user.geo.lon + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid + } + + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS auction should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() { + given: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } + def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy index 9d7657f452c..204f2eb7025 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy @@ -5,9 +5,10 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.amp.ConsentType import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent -import org.prebid.server.functional.util.privacy.gpp.UspV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsV1Consent import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 @@ -18,8 +19,8 @@ class GppAmpSpec extends PrivacyBaseSpec { def "PBS should populate bid request with regs when consent type is GPP and consent string, gppSid are present"() { given: "Default AmpRequest with consent_type = gpp" - def consentString = PBSUtils.randomString def gppSids = "${TCF_EU_V2.value},${USP_V1.value}" as String + def consentString = new TcfEuV2Consent.Builder().build().toString() def ampRequest = getGppAmpRequest(consentString, gppSids) def ampStoredRequest = BidRequest.defaultBidRequest.tap { setAccountId(ampRequest.account) @@ -30,12 +31,15 @@ class GppAmpSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) then: "Bidder request should contain consent string from amp request" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) assert bidderRequests.regs.gpp == consentString assert bidderRequests.regs.gppSid == [TCF_EU_V2.intValue, USP_V1.intValue] + + and: "Response shouldn't contain any warnings" + assert !ampResponse.ext?.warnings } def "PBS should populate bid request with regs.gppSid when consent type isn't GPP and gppSid is present"() { @@ -74,12 +78,16 @@ class GppAmpSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) then: "Bidder request shouldn't contain regs.gpp" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) assert !bidderRequests.regs.gpp assert !bidderRequests.regs.gppSid + + and: "Repose should contain warning" + assert ampResponse.ext?.warnings[PREBID]*.code == [999] + assert ampResponse.ext?.warnings[PREBID]*.message[0].startsWith("Failed to parse gppSid: \'${gppSids}\'") } def "PBS should emit warning when consent_string is invalid"() { @@ -125,7 +133,7 @@ class GppAmpSpec extends PrivacyBaseSpec { where: gppConsent << [new TcfEuV2Consent.Builder().build(), - new UspV1Consent.Builder().build()] + new UsV1Consent.Builder().build()] } def "PBS should copy consent_string to user.consent and set gdpr=1 when consent_string is valid and gppSid contains 2"() { @@ -157,7 +165,7 @@ class GppAmpSpec extends PrivacyBaseSpec { def "PBS should copy consent_string to user.us_privacy when consent_string contains us_privacy and gppSid contains 6"() { given: "Default amp request with valid consent_string and gpp consent_type" - def gppConsent = new UspV1Consent.Builder().build() + def gppConsent = new UsV1Consent.Builder().build() def gppSidIds = USP_V1.value def ampRequest = getGppAmpRequest(gppConsent.consentString, gppSidIds) @@ -181,7 +189,7 @@ class GppAmpSpec extends PrivacyBaseSpec { and: "Save storedRequest into DB" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -205,7 +213,7 @@ class GppAmpSpec extends PrivacyBaseSpec { and: "Save storedRequest into DB" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -215,6 +223,6 @@ class GppAmpSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain gpc value from header" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - assert !bidderRequest.regs.ext + assert !bidderRequest?.regs?.ext?.gpc } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy index 0283bc1887f..4eb78ee2140 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy @@ -2,13 +2,14 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent -import org.prebid.server.functional.util.privacy.gpp.UspV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsV1Consent import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 @@ -173,7 +174,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS should copy regs.gpp to regs.usPrivacy when gppSid contains 6, gpp is USP_V1 and regs.us_privacy isn't specified"() { given: "Default bid request with gpp and gppSid, without us_privacy" - def gppConsent = new UspV1Consent.Builder().build() + def gppConsent = new UsV1Consent.Builder().build() def bidRequest = BidRequest.defaultBidRequest.tap { regs = new Regs(gpp: gppConsent, gppSid: [USP_V1.intValue], usPrivacy: null) } @@ -190,7 +191,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS shouldn't copy regs.gpp to regs.usPrivacy when gppSid doesn't contain 6, gpp is USP_V1 and regs.us_privacy isn't specified"() { given: "Default bid request with gpp and gppSid, without us_privacy" def gppSidIds = [PBSUtils.getRandomNumberWithExclusion(USP_V1.intValue)] - def gpp = new UspV1Consent.Builder().build() + def gpp = new UsV1Consent.Builder().build() def bidRequest = BidRequest.defaultBidRequest.tap { regs = new Regs(gppSid: gppSidIds, gpp: gpp, usPrivacy: null) } @@ -211,7 +212,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS should emit warning when gppSid contains 6, gpp is USP_V1 and regs.gpp and regs.usPrivacy are different"() { given: "Default bid request with gpp, gppSid and usPrivacy" def gppSidIds = [USP_V1.intValue] - def gpp = new UspV1Consent.Builder().build() + def gpp = new UsV1Consent.Builder().build() def ccpaConsent = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: NOT_ENFORCED) def bidRequest = BidRequest.defaultBidRequest.tap { regs = new Regs(gppSid: gppSidIds, gpp: gpp, usPrivacy: ccpaConsent) @@ -235,7 +236,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS should populate gpc when header sec-gpc has value 1"() { given: "Default bid request with gpc" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } when: "PBS processes auction request with headers" @@ -252,7 +253,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS shouldn't populate gpc when header sec-gpc has #gpcInvalid value"() { given: "Default bid request with gpc" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } when: "PBS processes auction request with headers" @@ -260,7 +261,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain gpc from header" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - assert !bidderRequests.regs.ext + assert !bidderRequests?.regs?.ext?.gpc where: gpcInvalid << [PBSUtils.randomNumber as String, PBSUtils.randomNumber, PBSUtils.randomString, Boolean.TRUE] @@ -270,7 +271,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { given: "Default bid request with gpc" def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = randomGpc + regs.ext = new RegsExt(gpc: randomGpc) } when: "PBS processes auction request with headers" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy index 9f44c987360..c200c154bab 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy @@ -11,7 +11,7 @@ import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent -import org.prebid.server.functional.util.privacy.gpp.UspV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsV1Consent import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 @@ -186,7 +186,7 @@ class GppCookieSyncSpec extends BaseSpec { given: "Cookie sync request" def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { it.gppSid = USP_V1.value - it.gpp = new UspV1Consent.Builder().build() + it.gpp = new UsV1Consent.Builder().build() it.gdpr = null it.usPrivacy = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy index 8ba421405f7..db74b18ee48 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy @@ -3,27 +3,28 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.config.AccountGppConfig import org.prebid.server.functional.model.config.ActivityConfig import org.prebid.server.functional.model.config.EqualityValueRule +import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.config.InequalityValueRule import org.prebid.server.functional.model.config.LogicalRestrictedRule -import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition -import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.gpp.UspCaV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCoV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspUtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsVaV1Consent import org.prebid.server.functional.util.privacy.gpp.data.UsCaliforniaSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData @@ -41,6 +42,7 @@ import static org.prebid.server.functional.model.config.LogicalRestrictedRule.Lo import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_BELOW_13 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_FROM_13_TO_16 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.GPC +import static org.prebid.server.functional.model.config.UsNationalPrivacySection.PERSONAL_DATA_CONSENTS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_ACCOUNT_INFO import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_BIOMETRIC_ID import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_CITIZENSHIP_STATUS @@ -53,16 +55,21 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_RELIGIOUS_BELIEFS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.pricefloors.Country.CAN -import static org.prebid.server.functional.model.request.GppSectionId.USP_CA_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CO_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_NAT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_UT_V1 +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_VA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_UT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_VA_V1 import static org.prebid.server.functional.model.request.amp.ConsentType.GPP import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL @@ -71,29 +78,19 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE -import static org.prebid.server.functional.util.privacy.model.State.ONTARIO import static org.prebid.server.functional.util.privacy.model.State.ALABAMA +import static org.prebid.server.functional.util.privacy.model.State.ONTARIO class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { - private static final String ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT = "account.%s.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACCOUNT = "account.%s.activity.${FETCH_BIDS.metricValue}.disallowed.count" - private static final String ACTIVITY_RULES_PROCESSED_COUNT = "requests.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${FETCH_BIDS.metricValue}.disallowed.count" - private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${FETCH_BIDS.metricValue}.disallowed.count" - private static final String ALERT_GENERAL = "alerts.general" - def "PBS auction call when fetch bid activities is allowing should process bid request and update processed metrics"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE setAccountId(accountId) } - and: "Activities set with all bidders allowed" - def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, Activity.defaultActivity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -102,29 +99,31 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(FETCH_BIDS, Activity.defaultActivity), + new AllowActivities().tap { fetchBidsKebabCase = Activity.defaultActivity }, + new AllowActivities().tap { fetchBidsSnakeCase = Activity.defaultActivity }, + ] } def "PBS auction call when fetch bid activities is rejecting should skip call to restricted bidder and update disallowed metrics"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) ext.prebid.trace = VERBOSE } - and: "Activities set with all bidders rejected" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, activity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -133,22 +132,28 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(FETCH_BIDS, Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)])), + new AllowActivities().tap { fetchBidsKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + new AllowActivities().tap { fetchBidsSnakeCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + ] } def "PBS auction call when default activity setting set to false should skip call to restricted bidder"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) } @@ -161,10 +166,10 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 } def "PBS auction call when bidder allowed activities have invalid condition type should skip this rule and emit an error"() { @@ -173,7 +178,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) } @@ -186,7 +191,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def logs = activityPbsService.getLogsByTime(startTime) @@ -203,7 +208,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when first rule allowing in activities should call bid adapter"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) } @@ -220,16 +225,16 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) } def "PBS auction call when first rule disallowing in activities should skip call to restricted bidder"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) } @@ -246,16 +251,16 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 } def "PBS auction should process rule when gppSid doesn't intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { regs.gppSid = regsGppSid setAccountId(accountId) ext.prebid.trace = VERBOSE @@ -278,15 +283,15 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 where: regsGppSid | conditionGppSid @@ -297,18 +302,13 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should disallowed rule when gppSid intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { regs.gppSid = [USP_V1.intValue] setAccountId(accountId) ext.prebid.trace = VERBOSE } and: "Setup activity" - def condition = Condition.baseCondition.tap { - componentType = null - componentName = null - gppSid = [USP_V1.intValue] - } def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, activity) @@ -320,22 +320,37 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: + condition << [Condition.baseCondition.tap { + componentType = null + componentName = null + gppSid = [USP_V1.intValue] + }, Condition.baseCondition.tap { + componentType = null + componentName = null + gppSidKebabCase = [USP_V1.intValue] + }, Condition.baseCondition.tap { + componentType = null + componentName = null + gppSidSnakeCase = [USP_V1.intValue] + }] } def "PBS auction should process rule when device.geo doesn't intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.regs.gppSid = [USP_V1.intValue] it.ext.prebid.trace = VERBOSE @@ -360,19 +375,19 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 where: deviceGeo | conditionGeo - null | [USA.value] + null | [USA.ISOAlpha3] new Geo(country: USA) | null new Geo(region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: CAN, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] @@ -381,7 +396,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should disallowed rule when device.geo intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.regs.gppSid = null it.ext.prebid.trace = VERBOSE @@ -406,20 +421,20 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } @@ -428,10 +443,10 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def randomGpc = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = randomGpc + it.regs.ext = new RegsExt(gpc: randomGpc) } and: "Setup activity" @@ -451,25 +466,25 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 } def "PBS auction shouldn't disallowed rule when regs.ext.gpc doesn't intersect"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup activity" @@ -489,21 +504,21 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 } def "PBS auction should disallowed rule when header sec-gpc intersect with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE } @@ -525,16 +540,16 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests with headers" - activityPbsService.sendAuctionRequest(generalBidRequest, ["Sec-GPC": gpcHeader]) + activityPbsService.sendAuctionRequest(bidRequest, ["Sec-GPC": gpcHeader]) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 where: gpcHeader << [VALID_VALUE_FOR_GPC_HEADER as Integer, VALID_VALUE_FOR_GPC_HEADER] @@ -543,7 +558,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def "PBS auction shouldn't disallowed rule when header sec-gpc doesn't intersect with condition"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE } @@ -565,15 +580,15 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests with headers" - activityPbsService.sendAuctionRequest(generalBidRequest, ["Sec-GPC": gpcHeader]) + activityPbsService.sendAuctionRequest(bidRequest, ["Sec-GPC": gpcHeader]) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 where: gpcHeader << [1, "1"] @@ -584,8 +599,8 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] - regs.gpp = new UspNatV1Consent.Builder().build() + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV1Consent.Builder().build() } and: "Activities set for fetchBid with rejecting privacy regulation" @@ -642,13 +657,13 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { assert bidder.getBidderRequest(bidRequest.id) where: - gppConsent | gppSid - new UspNatV1Consent.Builder().build() | USP_NAT_V1 - new UspCaV1Consent.Builder().build() | USP_CA_V1 - new UspVaV1Consent.Builder().build() | USP_VA_V1 - new UspCoV1Consent.Builder().build() | USP_CO_V1 - new UspUtV1Consent.Builder().build() | USP_UT_V1 - new UspCtV1Consent.Builder().build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().build() | US_NAT_V1 + new UsCaV1Consent.Builder().build() | US_CA_V1 + new UsVaV1Consent.Builder().build() | US_VA_V1 + new UsCoV1Consent.Builder().build() | US_CO_V1 + new UsUtV1Consent.Builder().build() | US_UT_V1 + new UsCtV1Consent.Builder().build() | US_CT_V1 } def "PBS auction call when privacy regulation have duplicate should process request and update alerts metrics"() { @@ -657,7 +672,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def genericBidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) ext.prebid.trace = VERBOSE - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] } and: "Activities set for fetchBid with privacy regulation" @@ -670,11 +685,8 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Flush metrics" flushMetrics(activityPbsService) - and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) - def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) - - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) + and: "Save account with gpp config" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, gppAccountsConfig) accountDao.save(account) when: "PBS processes auction requests" @@ -685,15 +697,23 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 + + where: + gppAccountsConfig << [[new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false), + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true)], + [new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSidsKebabCase: [US_NAT_V1]), enabled: false), + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSidsKebabCase: []), enabled: true)], + [new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSidsSnakeCase: [US_NAT_V1]), enabled: false), + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSidsSnakeCase: []), enabled: true)]] } def "PBS auction call when privacy module contain invalid property should respond with an error"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String def genericBidRequest = BidRequest.defaultBidRequest.tap { - regs.gppSid = [USP_NAT_V1.intValue] - regs.gpp = new UspNatV1Consent.Builder().build() + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV1Consent.Builder().build() it.setAccountId(accountId) } @@ -721,10 +741,10 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation don't match custom requirement should call to bidder"() { given: "Default basic generic BidRequest" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -739,7 +759,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], accountLogic), [USP_NAT_V1], false) + it.config = accountLogic } and: "Existed account with privacy regulation setup" @@ -747,24 +767,29 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder should be called due to positive allow in activities" - assert bidder.getBidderRequest(generalBidRequest.id) + assert bidder.getBidderRequest(bidRequest.id) where: gpcValue | accountLogic - false | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NOTICE_PROVIDED)]) - true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NOTICE_PROVIDED)]) - true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_PROVIDED)]) + false | GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NOTICE_PROVIDED)])), [US_NAT_V1], false) + true | GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NOTICE_PROVIDED)])), [US_NAT_V1], false) + true | GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NOTICE_PROVIDED), new EqualityValueRule(SHARING_NOTICE, NOTICE_PROVIDED)])), [US_NAT_V1], false) + false | GppModuleConfig.getDefaultModuleConfigKebabCase(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NOTICE_PROVIDED)])), [US_NAT_V1], false) + true | GppModuleConfig.getDefaultModuleConfigKebabCase(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NOTICE_PROVIDED)])), [US_NAT_V1], false) + true | GppModuleConfig.getDefaultModuleConfigKebabCase(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NOTICE_PROVIDED), new EqualityValueRule(SHARING_NOTICE, NOTICE_PROVIDED)])), [US_NAT_V1], false) + false | GppModuleConfig.getDefaultModuleConfigSnakeCase(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NOTICE_PROVIDED)])), [US_NAT_V1], false) + true | GppModuleConfig.getDefaultModuleConfigSnakeCase(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NOTICE_PROVIDED)])), [US_NAT_V1], false) + true | GppModuleConfig.getDefaultModuleConfigSnakeCase(new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NOTICE_PROVIDED), new EqualityValueRule(SHARING_NOTICE, NOTICE_PROVIDED)])), [US_NAT_V1], false) } def "PBS auction call when privacy regulation match custom requirement should ignore call to bidder"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = BidRequest.defaultBidRequest.tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -780,7 +805,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], accountLogic), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], accountLogic), [US_NAT_V1], false) } and: "Existed account with privacy regulation setup" @@ -788,29 +813,29 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert bidder.getBidderRequests(generalBidRequest.id).size() == 0 + assert bidder.getBidderRequests(bidRequest.id).size() == 0 where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Generic BidRequest with gpp and account setup" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -826,7 +851,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -837,7 +862,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def error = thrown(PrebidServerException) @@ -846,13 +871,13 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when custom privacy regulation with normalizing should ignore call to bidder"() { given: "Generic BidRequest with gpp and account setup" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = BidRequest.defaultBidRequest.tap { regs.gppSid = [gppSid.intValue] regs.gpp = gppStateConsent.build() setAccountId(accountId) @@ -879,80 +904,80 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should be ignored" - assert !bidder.getBidderRequests(generalBidRequest.id) + assert !bidder.getBidderRequests(bidRequest.id) where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } @@ -990,7 +1015,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 } def "PBS amp call when bidder rejected in activities should skip call to restricted bidders and update disallowed metrics"() { @@ -1028,8 +1053,8 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 } def "PBS amp call when default activity setting set to false should skip call to restricted bidder"() { @@ -1224,7 +1249,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 } def "PBS amp should disallow rule when header gpc intersection with condition.gpc"() { @@ -1267,8 +1292,8 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1 } def "PBS amp call when privacy regulation match should call bid adapter"() { @@ -1281,8 +1306,8 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value - it.consentString = new UspNatV1Consent.Builder().build() + it.gppSid = US_NAT_V1.value + it.consentString = new UsNatV1Consent.Builder().build() it.consentType = GPP } @@ -1354,13 +1379,13 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { assert bidder.getBidderRequest(ampStoredRequest.id) where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS amp call when privacy regulation have duplicate should process request and update alerts metrics"() { @@ -1373,7 +1398,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value } and: "Activities set for fetchBid with privacy regulation" @@ -1387,7 +1412,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) @@ -1405,7 +1430,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when privacy module contain invalid property should respond with an error"() { @@ -1416,8 +1441,8 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value - it.consentString = new UspNatV1Consent.Builder().build() + it.gppSid = US_NAT_V1.value + it.consentString = new UsNatV1Consent.Builder().build() it.consentType = GPP } @@ -1455,10 +1480,10 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { } and: "amp request with link to account and gppSid" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -1508,7 +1533,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -1542,22 +1567,22 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { assert !bidder.getBidderRequests(ampStoredRequest.id).size() where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.gppSid = [USP_CT_V1.intValue] + regs.gppSid = [US_CT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -1565,7 +1590,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue } and: "Activities set with privacy regulation" @@ -1579,7 +1604,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([FETCH_BIDS], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -1604,7 +1629,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when custom privacy regulation with normalizing should ignore call to bidder"() { @@ -1656,74 +1681,74 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { assert !bidder.getBidderRequests(ampStoredRequest.id) where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSetUidSpec.groovy index 877676332fa..47a04509805 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSetUidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSetUidSpec.groovy @@ -6,10 +6,10 @@ import org.prebid.server.functional.model.request.setuid.SetuidRequest import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent -import org.prebid.server.functional.util.privacy.gpp.UspV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsV1Consent import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.request.GppSectionId.USP_NAT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID class GppSetUidSpec extends PrivacyBaseSpec { @@ -18,7 +18,7 @@ class GppSetUidSpec extends PrivacyBaseSpec { given: "Set uid request with invalid GPP" def setUidRequest = SetuidRequest.defaultSetuidRequest.tap { it.gpp = "Invalid_GPP_Consent_String" - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.uid = UUID.randomUUID().toString() it.gdpr = null it.gdprConsent = null @@ -68,7 +68,7 @@ class GppSetUidSpec extends PrivacyBaseSpec { assert response.uidsCookie.tempUIDs[GENERIC] where: - gpp << [new UspV1Consent.Builder().build().encodeSection(), + gpp << [new UsV1Consent.Builder().build().encodeSection(), new TcfEuV2Consent.Builder().build().encodeSection()] } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy index 4229a356033..602fbb87347 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy @@ -1,12 +1,17 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.model.config.AccountCcpaConfig +import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountGppConfig +import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.config.AccountSetting import org.prebid.server.functional.model.config.ActivityConfig import org.prebid.server.functional.model.config.EqualityValueRule +import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.config.InequalityValueRule import org.prebid.server.functional.model.config.LogicalRestrictedRule -import org.prebid.server.functional.model.config.GppModuleConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities @@ -15,12 +20,12 @@ import org.prebid.server.functional.model.request.cookiesync.CookieSyncRequest import org.prebid.server.functional.model.request.setuid.SetuidRequest import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.gpp.UspCaV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCoV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspUtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsVaV1Consent import org.prebid.server.functional.util.privacy.gpp.data.UsCaliforniaSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData @@ -38,6 +43,7 @@ import static org.prebid.server.functional.model.config.LogicalRestrictedRule.Lo import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_BELOW_13 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_FROM_13_TO_16 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.GPC +import static org.prebid.server.functional.model.config.UsNationalPrivacySection.PERSONAL_DATA_CONSENTS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_ACCOUNT_INFO import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_BIOMETRIC_ID import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_CITIZENSHIP_STATUS @@ -52,36 +58,39 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.request.GppSectionId.USP_CA_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CO_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_UT_V1 +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_NAT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_VA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_UT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_VA_V1 import static org.prebid.server.functional.model.request.auction.ActivityType.SYNC_USER import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_ALL +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_TFC_EU import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC -import static org.prebid.server.functional.util.privacy.model.State.MANITOBA +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.USA_IP import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ALASKA -import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_TFC_EU -import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL +import static org.prebid.server.functional.util.privacy.model.State.MANITOBA class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { - private static final String ACTIVITY_RULES_PROCESSED_COUNT = 'requests.activity.processedrules.count' - private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${SYNC_USER.metricValue}.disallowed.count" - private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${SYNC_USER.metricValue}.disallowed.count" - private static final String ALERT_GENERAL = "alerts.general" + private static final String GEO_LOCATION_REQUESTS = "geolocation_requests" + private static final String GEO_LOCATION_SUCCESSFUL = "geolocation_successful" private final static int INVALID_STATUS_CODE = 451 private final static String INVALID_STATUS_MESSAGE = "Unavailable For Legal Reasons." private static final Map GEO_LOCATION = ["geolocation.enabled" : "true", "geolocation.type" : "configuration", - "geolocation.configurations.[0].address-pattern": "209."] + "geolocation.configurations.[0].address-pattern": USA_IP.v4] def "PBS cookie sync call when bidder allowed in activities should include proper responded with bidders URLs and update processed metrics"() { given: "Cookie sync request with link to account" @@ -90,9 +99,6 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { it.account = accountId } - and: "Activities set for cookie sync with all bidders allowed" - def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.defaultActivity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -108,7 +114,13 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.defaultActivity), + new AllowActivities().tap { syncUserKebabCase = Activity.defaultActivity }, + new AllowActivities().tap { syncUserKebabCase = Activity.defaultActivity }, + ] } def "PBS cookie sync call when bidder rejected in activities should exclude bidders URLs with proper message and update disallowed metrics"() { @@ -118,10 +130,6 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { it.account = accountId } - and: "Activities set for cookie sync with all bidders rejected" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, activity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -137,8 +145,14 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)])), + new AllowActivities().tap { syncUserKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + new AllowActivities().tap { syncUserKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + ] } def "PBS cookie sync call when default activity setting set to false should exclude bidders URLs"() { @@ -282,7 +296,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 where: gppSid | conditionGppSid @@ -324,15 +338,15 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 } def "PBS cookie sync call when privacy regulation match and rejecting should exclude bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.account = accountId it.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -365,7 +379,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.account = accountId it.gpp = disallowGppLogic } @@ -393,22 +407,81 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSaleOptOut(1).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(0).setSaleOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(0).setTargetedAdvertisingOptOut(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build() + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build(), + new UsNatV1Consent.Builder().setSaleOptOut(1).setSaleOptOutNotice(1).setMspaServiceProviderMode(2).setMspaOptOutOptionMode(1).build(), + new UsNatV1Consent.Builder().setSaleOptOutNotice(2).setSaleOptOut(1).setMspaServiceProviderMode(2).setMspaOptOutOptionMode(1).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build(), + new UsNatV1Consent.Builder() + .setSharingNotice(2) + .setSharingOptOutNotice(1) + .setSharingOptOut(1) + .setMspaServiceProviderMode(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(2) + .setSharingOptOut(1) + .setSharingNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(2) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(1) + .setTargetedAdvertisingOptOutNotice(1) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build() + ] + } + + def "PBS cookie sync call when privacy module contain some part of disallow logic which violates GPP validation should exclude bidders URLs"() { + given: "Cookie sync request with link to account" + def accountId = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = disallowGppLogic + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should not contain any URLs for bidders" + assert !response.bidderStatus.userSync.url + + where: + disallowGppLogic << [ + 'DBABLA~BAAgAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAACAAAAAAA.QA' ] } @@ -442,20 +515,20 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert !response.bidderStatus.userSync.url where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS cookie sync call when privacy modules contain allowing settings should include proper responded with bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.account = accountId it.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -481,7 +554,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } @@ -489,7 +562,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.account = accountId it.gpp = regsGpp } @@ -516,14 +589,14 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert response.getBidderUserSync(GENERIC).userSync.url where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS cookie sync call when privacy regulation have duplicate should include proper responded with bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.account = accountId } @@ -535,7 +608,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([ruleUsGeneric])) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) and: "Flush metrics" @@ -553,14 +626,14 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS cookie sync call when privacy module contain invalid code should include proper responded with bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.gpp = SIMPLE_GPC_DISALLOW_LOGIC it.account = accountId } @@ -588,10 +661,10 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS cookie sync call when privacy regulation don't match custom requirement should include proper responded with bidders URLs"() { given: "Default basic generic BidRequest" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def accountId = PBSUtils.randomNumber as String def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.account = accountId it.gpp = gppConsent } @@ -631,7 +704,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.account = accountId it.gpp = gppConsent } @@ -661,22 +734,22 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert !response.bidderStatus.userSync.url where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS cookie sync call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Generic BidRequest with gpp and account setup" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.gpp = gppConsent setAccount(accountId) } @@ -692,7 +765,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([SYNC_USER], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([SYNC_USER], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -713,7 +786,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS cookie sync when custom privacy regulation with normalizing should exclude bidders URLs"() { @@ -752,74 +825,74 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert !response.bidderStatus.userSync.url where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } @@ -851,7 +924,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 } def "PBS setuid request when bidder restriction by activities should reject bidders with status code invalidStatusCode and update disallowed metrics"() { @@ -885,8 +958,8 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 } def "PBS setuid when default activity setting set to false should reject bidders with status code invalidStatusCode"() { @@ -1053,7 +1126,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 where: gppSid | conditionGppSid @@ -1104,7 +1177,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomString def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -1142,7 +1215,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomString def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.gpp = disallowGppLogic } @@ -1174,22 +1247,86 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSaleOptOut(1).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(0).setSaleOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(0).setTargetedAdvertisingOptOut(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build() + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build(), + new UsNatV1Consent.Builder().setSaleOptOut(1).setSaleOptOutNotice(1).setMspaServiceProviderMode(2).setMspaOptOutOptionMode(1).build(), + new UsNatV1Consent.Builder().setSaleOptOutNotice(2).setSaleOptOut(1).setMspaServiceProviderMode(2).setMspaOptOutOptionMode(1).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build(), + new UsNatV1Consent.Builder() + .setSharingNotice(2) + .setSharingOptOutNotice(1) + .setSharingOptOut(1) + .setMspaServiceProviderMode(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(2) + .setSharingOptOut(1) + .setSharingNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(2) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(1) + .setTargetedAdvertisingOptOutNotice(1) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build() + ] + } + + def "PBS setuid request when privacy module contain some part of disallow logic which violates GPP validation should reject bidders with status code invalidStatusCode"() { + given: "Cookie sync SetuidRequest with accountId" + def accountId = PBSUtils.randomString + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.gpp = disallowGppLogic + } + + and: "UIDS Cookie" + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes cookie sync request" + activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == INVALID_STATUS_CODE + assert exception.responseBody == INVALID_STATUS_MESSAGE + + where: + disallowGppLogic << [ + 'DBABLA~BAAgAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAACAAAAAAA.QA' ] } @@ -1228,13 +1365,13 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS setuid request when privacy modules contain allowing settings should respond with valid bidders UIDs cookies"() { @@ -1242,7 +1379,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomString def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -1270,7 +1407,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true), + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true), ] } @@ -1280,7 +1417,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomString def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.gpp = regsGpp } @@ -1309,7 +1446,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert response.responseBody where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS setuid request when privacy regulation have duplicate should respond with valid bidders UIDs cookies"() { @@ -1317,7 +1454,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomString def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value } and: "UIDS Cookie" @@ -1331,7 +1468,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([ruleUsGeneric])) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) and: "Flush metrics" @@ -1349,7 +1486,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS setuid request call when privacy module contain invalid code should respond with valid bidders UIDs cookies"() { @@ -1357,7 +1494,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomString def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -1388,10 +1525,10 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def "PBS setuid call when privacy regulation don't match custom requirement should respond with required UIDs cookies"() { given: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomNumber as String - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.account = accountId it.gpp = gppConsent } @@ -1434,7 +1571,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { given: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomNumber as String def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.account = accountId it.gpp = gppConsent } @@ -1469,24 +1606,24 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS setuid call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomString - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.account = accountId it.gpp = gppConsent - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value } and: "UIDS Cookie" @@ -1503,7 +1640,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([SYNC_USER], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([SYNC_USER], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -1524,7 +1661,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS setuid call when custom privacy regulation with normalizing should reject bidders with status code invalidStatusCode"() { @@ -1568,74 +1705,74 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } @@ -1680,15 +1817,15 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 where: - countyConfig | regionConfig | conditionGeo - null | null | ["$USA.value".toString()] - USA.value | ALABAMA.abbreviation | null - CAN.value | ALASKA.abbreviation | [USA.withState(ALABAMA)] - null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] - CAN.value | null | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + null | null | ["$USA.ISOAlpha3".toString()] + USA.ISOAlpha3 | ALABAMA.abbreviation | null + CAN.ISOAlpha3 | ALASKA.abbreviation | [USA.withState(ALABAMA)] + null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] + CAN.ISOAlpha3 | null | [USA.withState(ALABAMA)] } def "PBS setuid should process rule when geo doesn't intersection"() { @@ -1736,14 +1873,14 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 where: - countyConfig | regionConfig | conditionGeo - null | null | [USA.value] - CAN.value | ALASKA.abbreviation | [USA.withState(ALABAMA)] - null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] - CAN.value | null | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + null | null | [USA.ISOAlpha3] + CAN.ISOAlpha3 | ALASKA.abbreviation | [USA.withState(ALABAMA)] + null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] + CAN.ISOAlpha3 | null | [USA.withState(ALABAMA)] } def "PBS cookie sync should disallowed rule when device.geo intersection"() { @@ -1788,13 +1925,13 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 where: - countyConfig | regionConfig | conditionGeo - USA.value | null | [USA.value] - USA.value | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + USA.ISOAlpha3 | null | [USA.ISOAlpha3] + USA.ISOAlpha3 | ALABAMA.abbreviation | [USA.withState(ALABAMA)] } def "PBS setuid should disallowed rule when device.geo intersection"() { @@ -1842,8 +1979,60 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE where: - countyConfig | regionConfig | conditionGeo - USA.value | null | [USA.value] - USA.value | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + USA.ISOAlpha3 | null | [USA.ISOAlpha3] + USA.ISOAlpha3 | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + } + + def "PBS cookie sync should fetch geo once when gpp sync user and account require geo look up"() { + given: "Pbs config with geo location" + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + ["geolocation.configurations.[0].geo-info.country": USA.ISOAlpha3, + "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation]) + + and: "Cookie sync request with account connection" + def accountId = PBSUtils.randomNumber as String + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.account = accountId + it.gppSid = null + it.gdpr = null + } + + and: "Setup condition" + def condition = Condition.baseCondition.tap { + it.componentType = null + it.componentName = null + it.gppSid = null + it.geo = [USA.withState(ALABAMA)] + } + + and: "Set activity" + def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, activity) + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Set up account for allow activities" + def privacy = new AccountPrivacyConfig(ccpa: new AccountCcpaConfig(enabled: true), allowActivities: activities) + def accountConfig = new AccountConfig(privacy: privacy, settings: new AccountSetting(geoLookup: true)) + def account = new Account(uuid: accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes cookie sync request with header" + def response = prebidServerService + .sendCookieSyncRequest(cookieSyncRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Response should not contain any URLs for bidders" + assert !response.bidderStatus.userSync.url + + and: "Metrics for disallowed activities should be updated" + def metrics = prebidServerService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + and: "Metrics processed across activities should be updated" + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index ffecec814e2..42ee2290663 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -3,29 +3,26 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.config.AccountGppConfig import org.prebid.server.functional.model.config.ActivityConfig import org.prebid.server.functional.model.config.EqualityValueRule +import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.config.InequalityValueRule import org.prebid.server.functional.model.config.LogicalRestrictedRule -import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities -import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device -import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo -import org.prebid.server.functional.model.request.auction.User -import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.gpp.UspCaV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCoV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspUtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsVaV1Consent import org.prebid.server.functional.util.privacy.gpp.data.UsCaliforniaSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsNationalSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData @@ -34,7 +31,6 @@ import java.time.Instant import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED import static org.prebid.server.functional.model.config.DataActivity.NOTICE_PROVIDED @@ -45,6 +41,7 @@ import static org.prebid.server.functional.model.config.LogicalRestrictedRule.Lo import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_BELOW_13 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_FROM_13_TO_16 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.GPC +import static org.prebid.server.functional.model.config.UsNationalPrivacySection.PERSONAL_DATA_CONSENTS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_ACCOUNT_INFO import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_BIOMETRIC_ID import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_CITIZENSHIP_STATUS @@ -59,13 +56,19 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.request.GppSectionId.USP_CA_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CO_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_NAT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_UT_V1 +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_VA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_UT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_VA_V1 import static org.prebid.server.functional.model.request.amp.ConsentType.GPP import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL @@ -79,20 +82,10 @@ import static org.prebid.server.functional.util.privacy.model.State.ONTARIO class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { - private static final String ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT = "account.%s.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACCOUNT = "account.%s.activity.${TRANSMIT_EIDS.metricValue}.disallowed.count" - private static final String ACTIVITY_RULES_PROCESSED_COUNT = "requests.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${TRANSMIT_EIDS.metricValue}.disallowed.count" - private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${TRANSMIT_EIDS.metricValue}.disallowed.count" - private static final String ALERT_GENERAL = "alerts.general" - def "PBS auction call when transmit EIDS activities is allowing requests should leave EIDS fields in request and update proper metrics"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId) - - and: "Activities set with generic bidder allowed" - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.defaultActivity) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Flush metrics" flushMetrics(activityPbsService) @@ -102,26 +95,28 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.defaultActivity), + new AllowActivities().tap { transmitEidsKebabCase = Activity.defaultActivity }, + new AllowActivities().tap { transmitEidsSnakeCase = Activity.defaultActivity }, + ] } def "PBS auction call when transmit EIDS activities is rejecting requests should remove EIDS fields in request and update disallowed metrics"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId) - - and: "Allow activities setup" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity as Activity) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Flush metrics" flushMetrics(activityPbsService) @@ -131,10 +126,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids @@ -143,15 +138,21 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)])), + new AllowActivities().tap { transmitEidsSnakeCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + new AllowActivities().tap { transmitEidsKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + ] } def "PBS auction call when default activity setting set to false should remove EIDS fields from request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Allow activities setup" def activity = new Activity(defaultAction: false) @@ -162,10 +163,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids @@ -179,7 +180,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Activities set for transmit EIDS with bidder allowed without type" def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(conditions, isAllowed)]) @@ -190,7 +191,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def logs = activityPbsService.getLogsByTime(startTime) @@ -208,7 +209,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when first rule allowing in activities should leave EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Activity rules with same priority" def allowActivity = new ActivityRule(condition: Condition.baseCondition, allow: true) @@ -223,17 +224,17 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source } def "PBS auction call when first rule disallowing in activities should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Activities set for actions with Generic bidder rejected by hierarchy setup" def disallowActivity = new ActivityRule(condition: Condition.baseCondition, allow: false) @@ -248,10 +249,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids @@ -262,7 +263,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction shouldn't allow rule when gppSid not intersect"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { regs.gppSid = regsGppSid } @@ -285,16 +286,16 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 where: regsGppSid | conditionGppSid @@ -305,7 +306,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should allow rule when gppSid intersect"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { regs.gppSid = [USP_V1.intValue] } @@ -328,10 +329,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids @@ -340,15 +341,15 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 } def "PBS auction should process rule when device.geo doesn't intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.regs.gppSid = [USP_V1.intValue] it.device = new Device(geo: deviceGeo) } @@ -381,12 +382,12 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 where: deviceGeo | conditionGeo - null | [USA.value] + null | [USA.ISOAlpha3] new Geo(country: USA) | null new Geo(region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: CAN, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] @@ -395,7 +396,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should disallowed rule when device.geo intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) it.device = new Device(geo: deviceGeo) } @@ -432,13 +433,13 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } @@ -446,8 +447,8 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should process rule when regs.ext.gpc doesn't intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -477,17 +478,17 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 } def "PBS auction should disallowed rule when regs.ext.gpc intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def gpc = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = gpc + it.regs.ext = new RegsExt(gpc: gpc) } and: "Setup activity" @@ -521,16 +522,16 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 } def "PBS auction should process rule when header gpc doesn't intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -561,16 +562,16 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 } def "PBS auction should disallowed rule when header gpc intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup activity" @@ -604,16 +605,16 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 } def "PBS auction call when privacy regulation match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -632,10 +633,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids @@ -649,8 +650,8 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy module contain some part of disallow logic should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = disallowGppLogic } @@ -669,10 +670,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids !genericBidderRequest.user?.ext?.eids @@ -681,25 +682,73 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSaleOptOut(1).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(2).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(0).setSaleOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOut(1).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(0).setTargetedAdvertisingOptOut(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataLimitUseNotice(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(1) + .setMspaOptOutOptionMode(2) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(2) + .setSaleOptOut(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(2) + .setSharingOptOutNotice(1) + .setSharingOptOut(1) + .setMspaServiceProviderMode(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(2) + .setSharingOptOut(1) + .setSharingNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(2) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(1) + .setTargetedAdvertisingOptOutNotice(1) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(2) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(2) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 2) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(1, 0) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(2) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 1, religiousBeliefs: 1, healthInfo: 1, @@ -707,7 +756,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { citizenshipStatus: 1, unionMembership: 1, )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataLimitUseNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -722,7 +771,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -737,14 +786,14 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 1, biometricId: 1, idNumbers: 1, accountInfo: 1, communicationContents: 1 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 2, biometricId: 2, idNumbers: 2, @@ -754,10 +803,51 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS auction call when privacy module contain some part of disallow logic which violates GPP validation should remove EIDS fields in request"() { + given: "Default Generic BidRequests with EIDS fields and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = disallowGppLogic + } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + where: + disallowGppLogic << [ + 'DBABLA~BAAgAAAAAAA.QA', + 'DBABLA~BCAAAAAAAAA.QA', + 'DBABLA~BAAEAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA' + ] + } + def "PBS auction call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { regs.gppSid = [gppSid.intValue] regs.gpp = gppConsent } @@ -777,30 +867,30 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids !genericBidderRequest.user?.ext?.eids } where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS auction call when privacy modules contain allowing settings should leave EIDS fields in request"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -816,24 +906,24 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } def "PBS auction call when regs.gpp in request is allowing should leave EIDS fields in request"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = regsGpp } @@ -852,21 +942,21 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS auction call when privacy regulation have duplicate should leave EIDS fields in request and update alerts metrics"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] } and: "Activities set for transmitEIDS with privacy regulation" @@ -880,29 +970,29 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when privacy module contain invalid property should respond with an error"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -920,7 +1010,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def error = thrown(PrebidServerException) @@ -930,10 +1020,10 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation don't match custom requirement should leave EIDS fields in request"() { given: "Default basic generic BidRequest" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent } @@ -955,11 +1045,11 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) - assert genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source where: gpcValue | accountLogic @@ -972,8 +1062,8 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation match custom requirement should remove EIDS fields in request"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent } @@ -996,33 +1086,33 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(generalBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids !genericBidderRequest.user?.ext?.eids } where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Generic BidRequest with gpp and account setup" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { ext.prebid.trace = VERBOSE - regs.gppSid = [USP_CT_V1.intValue] + regs.gppSid = [US_CT_V1.intValue] regs.gpp = gppConsent } @@ -1037,7 +1127,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_EIDS], restrictedRule), [USP_CT_V1], false) + config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_EIDS], restrictedRule), [US_CT_V1], false) } and: "Flush metrics" @@ -1048,7 +1138,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def error = thrown(PrebidServerException) @@ -1057,13 +1147,13 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when custom privacy regulation with normalizing that match custom config should have empty EIDS fields"() { given: "Generic BidRequest with gpp and account setup" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { ext.prebid.trace = VERBOSE regs.gppSid = [gppSid.intValue] regs.gpp = gppStateConsent.build() @@ -1090,91 +1180,91 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty EIDS fields" - def genericBidderRequest = bidder.getBidderRequest(generalBidRequest.id) + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { !genericBidderRequest.user.eids !genericBidderRequest.user?.ext?.eids } where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } def "PBS amp call when transmit EIDS activities is allowing request should leave EIDS fields field in active request and update proper metrics"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1204,13 +1294,13 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 } def "PBS amp call when transmit EIDS activities is rejecting request should remove EIDS fields field in active request and update disallowed metrics"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1244,14 +1334,14 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 } def "PBS amp call when default activity setting set to false should remove EIDS fields from request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1288,7 +1378,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1326,7 +1416,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when first rule allowing in activities should leave EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1360,7 +1450,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when first rule disallowing in activities should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1397,8 +1487,8 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS amp should disallowed rule when header.gpc intersection with condition.gpc"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId).tap { - regs.ext.gpc = null + def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: null) } and: "amp request with link to account" @@ -1438,14 +1528,14 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 } def "PBS amp should allowed rule when gpc header doesn't intersection with condition.gpc"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1481,18 +1571,18 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 } def "PBS amp call when privacy regulation match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -1532,12 +1622,12 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy module contain some part of disallow logic should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = disallowGppLogic it.consentType = GPP } @@ -1573,25 +1663,73 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSaleOptOut(1).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(2).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(0).setSaleOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOut(1).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(0).setTargetedAdvertisingOptOut(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataLimitUseNotice(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(1) + .setMspaOptOutOptionMode(2) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(2) + .setSaleOptOut(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(2) + .setSharingOptOutNotice(1) + .setSharingOptOut(1) + .setMspaServiceProviderMode(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(2) + .setSharingOptOut(1) + .setSharingNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(2) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(1) + .setTargetedAdvertisingOptOutNotice(1) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(2) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(2) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 2) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(1, 0) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(2) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 1, religiousBeliefs: 1, healthInfo: 1, @@ -1599,7 +1737,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { citizenshipStatus: 1, unionMembership: 1, )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataLimitUseNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -1614,7 +1752,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -1629,14 +1767,14 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 1, biometricId: 1, idNumbers: 1, accountInfo: 1, communicationContents: 1 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 2, biometricId: 2, idNumbers: 2, @@ -1646,10 +1784,60 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when privacy module contain some part of disallow logic which violates GPP validation should remove EIDS fields in request"() { + given: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = disallowGppLogic + it.consentType = GPP + } + + and: "Activities set for transmitEIDS with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + where: + disallowGppLogic << [ + 'DBABLA~BAAgAAAAAAA.QA', + 'DBABLA~BCAAAAAAAAA.QA', + 'DBABLA~BAAEAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA' + ] + } + def "PBS amp call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1688,24 +1876,24 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { } where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS amp call when privacy modules contain allowing settings should leave EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -1735,19 +1923,19 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } def "PBS amp call when regs.gpp in request is allowing should leave EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = regsGpp it.consentType = GPP } @@ -1778,18 +1966,18 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS amp call when privacy regulation have duplicate should leave EIDS fields in request and update alerts metrics"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = "" it.consentType = GPP } @@ -1805,7 +1993,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) @@ -1824,18 +2012,18 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when privacy module contain invalid property should respond with an error"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -1869,13 +2057,13 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy regulation don't match custom requirement should leave EIDS fields in request"() { given: "Store bid request with link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gpp" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -1919,12 +2107,12 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy regulation match custom requirement should remove EIDS fields from request"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -1962,26 +2150,26 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { } where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Store bid request with link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gpp string" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.consentString = gppConsent it.consentType = GPP } @@ -1997,7 +2185,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_EIDS], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_EIDS], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -2022,13 +2210,13 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndEidsData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -2076,84 +2264,74 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { } where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } - - private static BidRequest givenBidRequestWithAccountAndEidsData(String accountId) { - BidRequest.getDefaultBidRequest().tap { - it.setAccountId(accountId) - it.ext.prebid.trace = VERBOSE - it.user = User.defaultUser - it.user.eids = [Eid.defaultEid] - it.user.ext = new UserExt(eids: [Eid.defaultEid]) - } - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy index 431b66ab6cd..e51a9b8f193 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy @@ -3,9 +3,9 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.config.AccountGppConfig import org.prebid.server.functional.model.config.ActivityConfig import org.prebid.server.functional.model.config.EqualityValueRule +import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.config.InequalityValueRule import org.prebid.server.functional.model.config.LogicalRestrictedRule -import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity @@ -13,14 +13,15 @@ import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.gpp.UspCaV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCoV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspUtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsVaV1Consent import org.prebid.server.functional.util.privacy.gpp.data.UsCaliforniaSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsNationalSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData @@ -29,7 +30,6 @@ import java.time.Instant import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.config.DataActivity.CONSENT import static org.prebid.server.functional.model.config.DataActivity.NOTICE_NOT_PROVIDED import static org.prebid.server.functional.model.config.DataActivity.NOTICE_PROVIDED @@ -40,6 +40,7 @@ import static org.prebid.server.functional.model.config.LogicalRestrictedRule.Lo import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_BELOW_13 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_FROM_13_TO_16 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.GPC +import static org.prebid.server.functional.model.config.UsNationalPrivacySection.PERSONAL_DATA_CONSENTS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_ACCOUNT_INFO import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_BIOMETRIC_ID import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_CITIZENSHIP_STATUS @@ -54,29 +55,32 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.request.GppSectionId.USP_CA_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CO_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_NAT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_UT_V1 +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_VA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_UT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_VA_V1 import static org.prebid.server.functional.model.request.amp.ConsentType.GPP import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO -import static org.prebid.server.functional.model.request.auction.PrivacyModule.* +import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_ALL +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_TFC_EU +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { - private static final String ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT = "account.%s.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACCOUNT = "account.%s.activity.${TRANSMIT_PRECISE_GEO.metricValue}.disallowed.count" - private static final String ACTIVITY_RULES_PROCESSED_COUNT = "requests.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${TRANSMIT_PRECISE_GEO.metricValue}.disallowed.count" - private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${TRANSMIT_PRECISE_GEO.metricValue}.disallowed.count" - private static final String ALERT_GENERAL = "alerts.general" - def "PBS auction call with bidder allowed in activities should not round lat/lon data and update processed metrics"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String @@ -85,9 +89,6 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { setAccountId(accountId) } - and: "Activities set with bidder allowed" - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.defaultActivity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -100,20 +101,40 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.defaultActivity), + new AllowActivities().tap { transmitPreciseGeoKebabCase = Activity.defaultActivity }, + new AllowActivities().tap { transmitPreciseGeoSnakeCase = Activity.defaultActivity }, + ] } def "PBS auction call with bidder rejected in activities should round lat/lon data to 2 digits and update disallowed metrics"() { @@ -124,10 +145,6 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { ext.prebid.trace = VERBOSE } - and: "Activities set with bidder allowed" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, activity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -140,21 +157,44 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)])), + new AllowActivities().tap { transmitPreciseGeoKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + new AllowActivities().tap { transmitPreciseGeoSnakeCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + ] } def "PBS auction call when default activity setting set to false should round lat/lon data to 2 digits"() { @@ -184,10 +224,24 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } } @@ -249,12 +303,25 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } @@ -284,14 +351,31 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } } @@ -327,20 +411,33 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 where: regsGppSid | conditionGppSid @@ -380,21 +477,38 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS auction should process rule when device.geo doesn't intersection"() { @@ -434,20 +548,33 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 where: deviceGeo | conditionGeo @@ -463,7 +590,10 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.setAccountId(accountId) it.regs.gppSid = [USP_V1.intValue] it.ext.prebid.trace = VERBOSE - it.device.geo = null + it.device.geo.tap { + country = null + region = null + } } and: "Setup condition" @@ -471,7 +601,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.componentType = null it.componentName = [PBSUtils.randomString] it.gppSid = [USP_V1.intValue] - it.geo = ["$USA.value".toString()] + it.geo = [USA.ISOAlpha3] } and: "Set activity" @@ -490,20 +620,33 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon - !bidderRequests.device.geo - !bidderRequests.device.geo } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS auction should disallowed rule when device.geo intersection"() { @@ -513,9 +656,9 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.setAccountId(accountId) it.regs.gppSid = null it.ext.prebid.trace = VERBOSE - it.device.geo = deviceGeo.tap { - lat = PBSUtils.getRandomDecimal(0, 90) - lon = PBSUtils.getRandomDecimal(0, 90) + it.device.geo.tap { + country = geoCountry + region = geoRegion } } @@ -543,27 +686,44 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 where: - deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] - new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] - new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] + geoCountry | geoRegion | conditionGeo + USA | null | [USA.ISOAlpha3] + USA | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + USA | ALABAMA.abbreviation | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } def "PBS auction should process rule when regs.ext.gpc doesn't intersection with condition.gpc"() { @@ -572,7 +732,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -598,20 +758,33 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS auction should disallowed rule when regs.ext.gpc intersection with condition.gpc"() { @@ -621,7 +794,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.setAccountId(accountId) it.regs.gppSid = null it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = "1" + it.regs.ext = new RegsExt(gpc: "1") } and: "Setup activity" @@ -652,17 +825,35 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS auction should process rule when header gpc doesn't intersection with condition.gpc"() { @@ -671,7 +862,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -697,20 +888,33 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS auction should process rule when header gpc intersection with condition.gpc"() { @@ -719,7 +923,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup condition" @@ -745,21 +949,38 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS auction call when privacy regulation match and rejecting should round lat/lon data to 2 digits"() { @@ -767,7 +988,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -790,14 +1011,31 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: @@ -809,7 +1047,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = disallowGppLogic } @@ -832,37 +1070,58 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataLimitUseNotice(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(2) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() .setSensitiveDataLimitUseNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( geolocation: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( geolocation: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( geolocation: 1 @@ -898,24 +1157,41 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - bidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - bidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - bidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - bidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS auction call when privacy modules contain allowing settings should not round lat/lon data"() { @@ -923,7 +1199,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -943,12 +1219,25 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } @@ -956,7 +1245,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } @@ -965,7 +1254,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = regsGpp } @@ -988,18 +1277,32 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS auction call when privacy regulation have duplicate should process request and update alerts metrics"() { @@ -1007,7 +1310,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] } and: "Activities set for transmitPreciseGeo with privacy regulation" @@ -1021,7 +1324,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) @@ -1032,19 +1335,33 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == bidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == bidRequest.device.geo.lat - bidderRequests.device.geo.lon == bidRequest.device.geo.lon bidderRequests.user.geo.lat == bidRequest.user.geo.lat bidderRequests.user.geo.lon == bidRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when privacy module contain invalid code should respond with an error"() { @@ -1052,7 +1369,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) - regs.gppSid = [USP_NAT_V1.intValue] + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -1080,10 +1397,10 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation don't match custom requirement should not round lat/lon data"() { given: "Default basic generic BidRequest" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def accountId = PBSUtils.randomNumber as String - def genericBidRequest = bidRequestWithGeo.tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = bidRequestWithGeo.tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -1107,18 +1424,32 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain not rounded geo data for device and user" - def bidderRequests = bidder.getBidderRequest(genericBidRequest.id) - + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == bidRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + + deviceBidderRequest.geo.lat == bidRequest.device.geo.lat + deviceBidderRequest.geo.lon == bidRequest.device.geo.lon + deviceBidderRequest.geo.country == bidRequest.device.geo.country + deviceBidderRequest.geo.region == bidRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == bidRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == bidRequest.device.geo.metro + deviceBidderRequest.geo.city == bidRequest.device.geo.city + deviceBidderRequest.geo.zip == bidRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == bidRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == bidRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == bidRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == genericBidRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == genericBidRequest.device.geo.lat - bidderRequests.device.geo.lon == genericBidRequest.device.geo.lon - bidderRequests.user.geo.lat == genericBidRequest.user.geo.lat - bidderRequests.user.geo.lon == genericBidRequest.user.geo.lon + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: @@ -1132,8 +1463,8 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation match custom requirement should round lat/lon data to 2 digits"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = bidRequestWithGeo.tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = bidRequestWithGeo.tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -1158,38 +1489,55 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain rounded geo data for device and user to 2 digits" - def bidderRequests = bidder.getBidderRequest(generalBidRequest.id) - + def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - generalBidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - generalBidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - generalBidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - generalBidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Generic BidRequest with gpp and account setup" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String - def generalBidRequest = bidRequestWithGeo.tap { + def bidRequest = bidRequestWithGeo.tap { ext.prebid.trace = VERBOSE - regs.gppSid = [USP_CT_V1.intValue] + regs.gppSid = [US_CT_V1.intValue] regs.gpp = gppConsent setAccountId(accountId) } @@ -1207,7 +1555,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_PRECISE_GEO], restrictedRule), [USP_CT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_PRECISE_GEO], restrictedRule), [US_CT_V1], false) } and: "Flush metrics" @@ -1218,7 +1566,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def error = thrown(PrebidServerException) @@ -1227,13 +1575,13 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when custom privacy regulation with normalizing should change request consent and call to bidder"() { given: "Generic BidRequest with gpp and account setup" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = bidRequestWithGeo.tap { + def bidRequest = bidRequestWithGeo.tap { ext.prebid.trace = VERBOSE regs.gppSid = [gppSid.intValue] regs.gpp = gppStateConsent.build() @@ -1264,89 +1612,106 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain rounded geo data for device and user to 2 digits" - def bidderRequests = bidder.getBidderRequest(generalBidRequest.id) - + def bidderRequests = bidder.getBidderRequest(bidRequest.id) verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - generalBidRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - generalBidRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - generalBidRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - generalBidRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == bidRequest.device.geo.country + bidderRequests.device.geo.region == bidRequest.device.geo.region + bidderRequests.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } @@ -1382,19 +1747,32 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS amp call with bidder rejected in activities should round lat/lon data to 2 digits and update disallowed metrics"() { @@ -1429,20 +1807,37 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS amp call when default activity setting set to false should round lat/lon data to 2 digits"() { @@ -1474,14 +1869,31 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } } @@ -1561,12 +1973,25 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } @@ -1605,14 +2030,31 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } } @@ -1653,19 +2095,32 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS amp should disallow rule when header gpc intersection with condition.gpc"() { @@ -1706,20 +2161,37 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 } def "PBS amp call when privacy regulation match and rejecting should round lat/lon data to 2 digits"() { @@ -1730,7 +2202,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -1758,14 +2230,31 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } where: @@ -1780,7 +2269,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = disallowGppLogic it.consentType = GPP } @@ -1808,37 +2297,58 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataLimitUseNotice(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), + new UsNatV1Consent.Builder().setKnownChildSensitiveDataConsents(1, 0).build(), + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(2) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() .setSensitiveDataLimitUseNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( geolocation: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( geolocation: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( geolocation: 1 @@ -1882,24 +2392,41 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS amp call when privacy modules contain allowing settings should not round lat/lon data"() { @@ -1910,7 +2437,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -1935,12 +2462,25 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } @@ -1948,7 +2488,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } @@ -1960,7 +2500,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = regsGpp it.consentType = GPP } @@ -1988,18 +2528,31 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS amp call when privacy regulation have duplicate should process request and update alerts metrics"() { @@ -2010,7 +2563,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value } and: "Activities set for transmitPreciseGeo with privacy regulation" @@ -2024,7 +2577,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) @@ -2039,19 +2592,32 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when privacy module contain invalid code should respond with an error"() { @@ -2062,7 +2628,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -2097,7 +2663,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy regulation don't match custom requirement should not round lat/lon data in request"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def ampStoredRequest = bidRequestWithGeo.tap { setAccountId(accountId) } @@ -2105,7 +2671,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -2137,12 +2703,25 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain not rounded geo data for device and user" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - + def deviceBidderRequest = bidderRequests.device + verifyAll { + deviceBidderRequest.ip == ampStoredRequest.device.ip + deviceBidderRequest.ipv6 == "af47:892b:3e98:b49a::" + deviceBidderRequest.geo.lat == ampStoredRequest.device.geo.lat + deviceBidderRequest.geo.lon == ampStoredRequest.device.geo.lon + deviceBidderRequest.geo.country == ampStoredRequest.device.geo.country + deviceBidderRequest.geo.region == ampStoredRequest.device.geo.region + deviceBidderRequest.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + deviceBidderRequest.geo.metro == ampStoredRequest.device.geo.metro + deviceBidderRequest.geo.city == ampStoredRequest.device.geo.city + deviceBidderRequest.geo.zip == ampStoredRequest.device.geo.zip + deviceBidderRequest.geo.accuracy == ampStoredRequest.device.geo.accuracy + deviceBidderRequest.geo.ipservice == ampStoredRequest.device.geo.ipservice + deviceBidderRequest.geo.ext == ampStoredRequest.device.geo.ext + } + + and: "Bidder request user.geo.{lat,lon} shouldn't mask" verifyAll { - bidderRequests.device.ip == ampStoredRequest.device.ip - bidderRequests.device.ipv6 == "af47:892b:3e98:b49a::" - bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat - bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } @@ -2165,7 +2744,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -2198,31 +2777,48 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def ampStoredRequest = bidRequestWithGeo.tap { setAccountId(accountId) } @@ -2230,7 +2826,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.consentString = gppConsent it.consentType = GPP } @@ -2246,7 +2842,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_PRECISE_GEO], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_PRECISE_GEO], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -2271,7 +2867,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { @@ -2321,85 +2917,102 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { then: "Bidder request should contain rounded geo data for device and user to 2 digits" def bidderRequests = bidder.getBidderRequest(ampStoredRequest.id) - verifyAll { bidderRequests.device.ip == "43.77.114.0" bidderRequests.device.ipv6 == "af47:892b:3e98:b400::" - ampStoredRequest.device.geo.lat.round(2) == bidderRequests.device.geo.lat - ampStoredRequest.device.geo.lon.round(2) == bidderRequests.device.geo.lon - ampStoredRequest.user.geo.lat.round(2) == bidderRequests.user.geo.lat - ampStoredRequest.user.geo.lon.round(2) == bidderRequests.user.geo.lon + bidderRequests.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequests.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequests.device.geo.country == ampStoredRequest.device.geo.country + bidderRequests.device.geo.region == ampStoredRequest.device.geo.region + bidderRequests.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask several geo fields" + verifyAll { + !bidderRequests.device.geo.metro + !bidderRequests.device.geo.city + !bidderRequests.device.geo.zip + !bidderRequests.device.geo.accuracy + !bidderRequests.device.geo.ipservice + !bidderRequests.device.geo.ext + } + + and: "Bidder request shouldn't mask geo.{lat,lon} fields" + verifyAll { + bidderRequests.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequests.user.geo.lon == ampStoredRequest.user.geo.lon } where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitTidActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitTidActivitiesSpec.groovy index ea3a5db4fa8..df0d0ab531f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitTidActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitTidActivitiesSpec.groovy @@ -12,18 +12,16 @@ import org.prebid.server.functional.util.PBSUtils import java.time.Instant -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_TID import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { - private static final String ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT = "account.%s.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACCOUNT = "account.%s.activity.${TRANSMIT_TID.metricValue}.disallowed.count" - private static final String ACTIVITY_RULES_PROCESSED_COUNT = "requests.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${TRANSMIT_TID.metricValue}.disallowed.count" - private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${TRANSMIT_TID.metricValue}.disallowed.count" - def "PBS auction should generate id for bidRequest.(source/imp[0].ext).tid when ext.prebid.createTids=null and transmit activity allowed"() { given: "Bid requests without TID fields and account id" def accountId = PBSUtils.randomNumber as String @@ -37,9 +35,6 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { source = new Source(tid: null) } - and: "Activities set with generic bidder allowed" - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_TID, Activity.defaultActivity) - and: "Flush metrics" flushMetrics(activityPbsService) @@ -60,11 +55,17 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_TID)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_TID)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_TID, Activity.defaultActivity), + new AllowActivities().tap { transmitTidKebabCase = Activity.defaultActivity }, + new AllowActivities().tap { transmitTidSnakeCase = Activity.defaultActivity }, + ] } - def "PBS auction should generate id for bidRequest.(source/imp[0].ext).tid when ext.prebid.createTids=true and transmit activity #transmitActivityAllowStatus"() { + def "PBS auction should generate id for bidRequest.(source/imp[0].ext).tid when ext.prebid.createTids=true and transmit activity"() { given: "Bid requests without TID fields and account id" def accountId = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { @@ -77,10 +78,6 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { source = new Source(tid: null) } - and: "Activities set with bidder disallowed" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, transmitActivityAllowStatus)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_TID, activity) - and: "Save account config with allow activities into DB" def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) accountDao.save(account) @@ -96,8 +93,11 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { bidderRequest.source.tid } - where: - transmitActivityAllowStatus << [true, false] + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_TID, Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)])), + new AllowActivities().tap { transmitTidKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + new AllowActivities().tap { transmitTidSnakeCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + ] } def "PBS auction shouldn't generate id for bidRequest.(source/imp[0].ext).tid and don't change schain in request when ext.prebid.createTids=false and transmit activity allowed and schain specified in request"() { @@ -224,9 +224,9 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_TID)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_TID)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_TID)] == 1 } def "PBS auction should remove bidRequest.(source/imp[0].ext).tid and don't change schain in request when ext.prebid.createTids=#createTid and defaultAction=false and schain specified in request"() { @@ -300,11 +300,19 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { "contains conditional rule with empty array").size() == 1 where: - conditions | isAllowed - new Condition(componentType: []) | true - new Condition(componentType: []) | false - new Condition(componentName: []) | true - new Condition(componentName: []) | false + conditions | isAllowed + new Condition(componentType: []) | true + new Condition(componentType: []) | false + new Condition(componentName: []) | true + new Condition(componentName: []) | false + new Condition(componentTypeKebabCase: []) | true + new Condition(componentTypeKebabCase: []) | false + new Condition(componentNameKebabCase: []) | true + new Condition(componentNameKebabCase: []) | false + new Condition(componentTypeSnakeCase: []) | true + new Condition(componentTypeSnakeCase: []) | false + new Condition(componentNameSnakeCase: []) | true + new Condition(componentNameSnakeCase: []) | false } def "PBS auction should generate bidRequest.(source/imp[0].ext).tid when first rule allow=true and bidRequest.(source/imp[0].ext).tid=null"() { @@ -418,7 +426,7 @@ class GppTransmitTidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_TID)] == 1 } def "PBS amp should generate id for bidRequest.(source/imp[0].ext).tid when ext.prebid.createTids=true and transmit activity allowed"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index 5cd227ccef1..bbb019d515d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -4,13 +4,14 @@ import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.config.AccountGppConfig import org.prebid.server.functional.model.config.ActivityConfig import org.prebid.server.functional.model.config.EqualityValueRule +import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.config.InequalityValueRule import org.prebid.server.functional.model.config.LogicalRestrictedRule -import org.prebid.server.functional.model.config.GppModuleConfig import org.prebid.server.functional.model.config.Purpose import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.config.PurposeEid import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities @@ -20,18 +21,18 @@ import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.request.auction.UserExtData -import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.gpp.UspCaV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCoV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspCtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspUtV1Consent -import org.prebid.server.functional.util.privacy.gpp.UspVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsVaV1Consent import org.prebid.server.functional.util.privacy.gpp.data.UsCaliforniaSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsNationalSensitiveData import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData @@ -50,6 +51,7 @@ import static org.prebid.server.functional.model.config.LogicalRestrictedRule.Lo import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_BELOW_13 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.CHILD_CONSENTS_FROM_13_TO_16 import static org.prebid.server.functional.model.config.UsNationalPrivacySection.GPC +import static org.prebid.server.functional.model.config.UsNationalPrivacySection.PERSONAL_DATA_CONSENTS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_ACCOUNT_INFO import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_BIOMETRIC_ID import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_CITIZENSHIP_STATUS @@ -62,16 +64,21 @@ import static org.prebid.server.functional.model.config.UsNationalPrivacySection import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SENSITIVE_DATA_RELIGIOUS_BELIEFS import static org.prebid.server.functional.model.config.UsNationalPrivacySection.SHARING_NOTICE -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA -import static org.prebid.server.functional.model.request.GppSectionId.USP_CA_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CO_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_CT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_UT_V1 +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.ALERT_GENERAL +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_NAT_V1 -import static org.prebid.server.functional.model.request.GppSectionId.USP_VA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_CT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_UT_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_VA_V1 import static org.prebid.server.functional.model.request.amp.ConsentType.GPP import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL @@ -85,20 +92,10 @@ import static org.prebid.server.functional.util.privacy.model.State.ONTARIO class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { - private static final String ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT = "account.%s.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACCOUNT = "account.%s.activity.${TRANSMIT_UFPD.metricValue}.disallowed.count" - private static final String ACTIVITY_RULES_PROCESSED_COUNT = "requests.activity.processedrules.count" - private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${TRANSMIT_UFPD.metricValue}.disallowed.count" - private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${TRANSMIT_UFPD.metricValue}.disallowed.count" - private static final String ALERT_GENERAL = "alerts.general" - def "PBS auction call when transmit UFPD activities is allowing requests should leave UFPD fields in request and update proper metrics"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) - - and: "Activities set with generic bidder allowed" - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.defaultActivity) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Flush metrics" flushMetrics(activityPbsService) @@ -108,42 +105,47 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.defaultActivity), + new AllowActivities().tap { transmitUfpdSnakeCase = Activity.defaultActivity }, + new AllowActivities().tap { transmitUfpdKebabCase = Activity.defaultActivity }, + ] } def "PBS auction call when transmit UFPD activities is rejecting requests should remove UFPD fields in request and update disallowed metrics"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) - - and: "Allow activities setup" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity as Activity) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Flush metrics" flushMetrics(activityPbsService) @@ -153,38 +155,48 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids } and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + + where: "Activities fields name in different case" + activities << [AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)])), + new AllowActivities().tap { transmitUfpdKebabCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + new AllowActivities().tap { transmitUfpdSnakeCase = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) }, + ] } def "PBS auction call when default activity setting set to false should remove UFPD fields from request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Allow activities setup" def activity = new Activity(defaultAction: false) @@ -195,27 +207,30 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids } def "PBS auction call when bidder allowed activities have empty condition type should skip this rule and emit an error"() { @@ -224,7 +239,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Activities set for transmit ufpd with bidder allowed without type" def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(conditions, isAllowed)]) @@ -235,7 +250,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def logs = activityPbsService.getLogsByTime(startTime) @@ -253,7 +268,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when first rule allowing in activities should leave UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Activity rules with same priority" def allowActivity = new ActivityRule(condition: Condition.baseCondition, allow: true) @@ -268,33 +283,36 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids } def "PBS auction call when first rule disallowing in activities should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Activities set for actions with Generic bidder rejected by hierarchy setup" def disallowActivity = new ActivityRule(condition: Condition.baseCondition, allow: false) @@ -309,32 +327,35 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids } def "PBS auction shouldn't allow rule when gppSid not intersect"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { regs.gppSid = regsGppSid } @@ -357,32 +378,35 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 where: regsGppSid | conditionGppSid @@ -393,7 +417,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should allow rule when gppSid intersect"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { regs.gppSid = [USP_V1.intValue] } @@ -416,38 +440,42 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.geo + !bidderRequest.user.data + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 } def "PBS auction should process rule when device.geo doesn't intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.regs.gppSid = [USP_V1.intValue] it.device = new Device(geo: deviceGeo) } @@ -489,19 +517,22 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { bidderRequest.user.buyeruid == bidRequest.user.buyeruid bidderRequest.user.yob == bidRequest.user.yob bidderRequest.user.gender == bidRequest.user.gender - bidderRequest.user.eids[0].source == bidRequest.user.eids[0].source bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 where: deviceGeo | conditionGeo - null | [USA.value] + null | [USA.ISOAlpha3] new Geo(country: USA) | null new Geo(region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: CAN, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] @@ -510,7 +541,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should disallowed rule when device.geo intersection"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) it.device = new Device(geo: deviceGeo) } @@ -552,19 +583,23 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { !bidderRequest.user.buyeruid !bidderRequest.user.yob !bidderRequest.user.gender - !bidderRequest.user.eids + !bidderRequest.user.geo !bidderRequest.user.data + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } @@ -572,8 +607,8 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction should process rule when regs.ext.gpc doesn't intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -612,24 +647,27 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { bidderRequest.user.buyeruid == bidRequest.user.buyeruid bidderRequest.user.yob == bidRequest.user.yob bidderRequest.user.gender == bidRequest.user.gender - bidderRequest.user.eids[0].source == bidRequest.user.eids[0].source bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 } def "PBS auction should disallowed rule when regs.ext.gpc intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def gpc = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = gpc + it.regs.ext = new RegsExt(gpc: gpc) } and: "Setup activity" @@ -668,22 +706,26 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { !bidderRequest.user.buyeruid !bidderRequest.user.yob !bidderRequest.user.gender - !bidderRequest.user.eids + !bidderRequest.user.geo !bidderRequest.user.data + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 } def "PBS auction should process rule when header gpc doesn't intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -723,23 +765,26 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { bidderRequest.user.buyeruid == bidRequest.user.buyeruid bidderRequest.user.yob == bidRequest.user.yob bidderRequest.user.gender == bidRequest.user.gender - bidderRequest.user.eids[0].source == bidRequest.user.eids[0].source bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo.zip == bidRequest.user.geo.zip bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 - assert metrics[ACTIVITY_PROCESSED_RULES_FOR_ACCOUNT.formatted(accountId)] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 } def "PBS auction should disallowed rule when header gpc intersection with condition.gpc"() { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String - def bidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup activity" @@ -778,22 +823,26 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { !bidderRequest.user.buyeruid !bidderRequest.user.yob !bidderRequest.user.gender - !bidderRequest.user.eids + !bidderRequest.user.geo !bidderRequest.user.data + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 } def "PBS auction call when privacy regulation match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -812,27 +861,30 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } @@ -840,8 +892,8 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy module contain some part of disallow logic should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = disallowGppLogic } @@ -860,49 +912,100 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSaleOptOut(1).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(2).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(0).setSaleOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOut(1).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(0).setTargetedAdvertisingOptOut(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataLimitUseNotice(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(1) + .setMspaOptOutOptionMode(2) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(2) + .setSaleOptOut(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(2) + .setSharingOptOutNotice(1) + .setSharingOptOut(1) + .setMspaServiceProviderMode(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(2) + .setSharingOptOut(1) + .setSharingNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(2) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(1) + .setTargetedAdvertisingOptOutNotice(1) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(2) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(2) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 2) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(1, 0) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(2) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 1, religiousBeliefs: 1, healthInfo: 1, @@ -910,7 +1013,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { citizenshipStatus: 1, unionMembership: 1, )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataLimitUseNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -925,7 +1028,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -940,14 +1043,14 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 1, biometricId: 1, idNumbers: 1, accountInfo: 1, communicationContents: 1 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 2, biometricId: 2, idNumbers: 2, @@ -957,10 +1060,66 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS auction call when privacy module contain some part of disallow logic which violates GPP validation should remove UFPD fields in request"() { + given: "Default Generic BidRequests with UFPD fields and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = disallowGppLogic + } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have empty UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll { + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.ext + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + where: + disallowGppLogic << [ + 'DBABLA~BAAgAAAAAAA.QA', + 'DBABLA~BCAAAAAAAAA.QA', + 'DBABLA~BAAEAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA' + ] + } + def "PBS auction call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { regs.gppSid = [gppSid.intValue] regs.gpp = gppConsent } @@ -980,42 +1139,45 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS auction call when privacy modules contain allowing settings should leave UFPD fields in request"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -1031,41 +1193,44 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) and: "Generic bidder should be called due to positive allow in activities" verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } def "PBS auction call when regs.gpp in request is allowing should leave UFPD fields in request"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = regsGpp } @@ -1084,38 +1249,41 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) and: "Generic bidder should be called due to positive allow in activities" verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS auction call when privacy regulation have duplicate should leave UFPD fields in request and update alerts metrics"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] } and: "Activities set for transmitUfpd with privacy regulation" @@ -1129,46 +1297,49 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) and: "Generic bidder should be called due to positive allow in activities" verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when privacy module contain invalid property should respond with an error"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC } @@ -1186,7 +1357,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def error = thrown(PrebidServerException) @@ -1196,10 +1367,10 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation don't match custom requirement should leave UFPD fields in request"() { given: "Default basic generic BidRequest" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent } @@ -1221,27 +1392,30 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == genericBidRequest.device.didsha1 - genericBidderRequest.device.didmd5 == genericBidRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == genericBidRequest.device.dpidsha1 - genericBidderRequest.device.ifa == genericBidRequest.device.ifa - genericBidderRequest.device.macsha1 == genericBidRequest.device.macsha1 - genericBidderRequest.device.macmd5 == genericBidRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == genericBidRequest.device.dpidmd5 - genericBidderRequest.user.id == genericBidRequest.user.id - genericBidderRequest.user.buyeruid == genericBidRequest.user.buyeruid - genericBidderRequest.user.yob == genericBidRequest.user.yob - genericBidderRequest.user.gender == genericBidRequest.user.gender - genericBidderRequest.user.eids[0].source == genericBidRequest.user.eids[0].source - genericBidderRequest.user.data == genericBidRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == genericBidRequest.user.ext.data.buyeruid + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo == bidRequest.user.geo + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: gpcValue | accountLogic false | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NOTICE_PROVIDED)]) @@ -1253,8 +1427,8 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS auction call when privacy regulation match custom requirement should remove UFPD fields in request"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [USP_NAT_V1.intValue] + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] regs.gpp = gppConsent } @@ -1277,45 +1451,48 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(generalBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS auction call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Generic BidRequest with gpp and account setup" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { ext.prebid.trace = VERBOSE - regs.gppSid = [USP_CT_V1.intValue] + regs.gppSid = [US_CT_V1.intValue] regs.gpp = gppConsent } @@ -1330,7 +1507,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_UFPD], restrictedRule), [USP_CT_V1], false) + config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_UFPD], restrictedRule), [US_CT_V1], false) } and: "Flush metrics" @@ -1341,7 +1518,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" def error = thrown(PrebidServerException) @@ -1350,13 +1527,13 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS auction call when custom privacy regulation with normalizing that match custom config should have empty UFPD fields"() { given: "Generic BidRequest with gpp and account setup" def accountId = PBSUtils.randomNumber as String - def generalBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { + def bidRequest = getBidRequestWithPersonalData(accountId).tap { ext.prebid.trace = VERBOSE regs.gppSid = [gppSid.intValue] regs.gpp = gppStateConsent.build() @@ -1383,103 +1560,106 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(generalBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(generalBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } def "PBS amp call when transmit UFPD activities is allowing request should leave UFPD fields field in active request and update proper metrics"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1504,34 +1684,37 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 } def "PBS amp call when transmit UFPD activities is rejecting request should remove UFPD fields field in active request and update disallowed metrics"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1557,34 +1740,37 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 } def "PBS amp call when default activity setting set to false should remove UFPD fields from request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1607,24 +1793,27 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids } def "PBS amp call when bidder allowed activities have empty condition type should skip this rule and emit an error"() { @@ -1633,7 +1822,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1671,7 +1860,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when first rule allowing in activities should leave UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1698,30 +1887,33 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids } def "PBS amp call when first rule disallowing in activities should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1748,30 +1940,33 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids } def "PBS amp should disallowed rule when header.gpc intersection with condition.gpc"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.ext.gpc = null + def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: null) } and: "amp request with link to account" @@ -1803,34 +1998,37 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest, ["Sec-GPC": VALID_VALUE_FOR_GPC_HEADER]) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 } def "PBS amp should allowed rule when gpc header doesn't intersection with condition.gpc"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -1861,39 +2059,42 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest, ["Sec-GPC": VALID_VALUE_FOR_GPC_HEADER]) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids and: "Metrics processed across activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 } def "PBS amp call when privacy regulation match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -1920,24 +2121,27 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + where: privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } @@ -1945,12 +2149,12 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy module contain some part of disallow logic should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = disallowGppLogic it.consentType = GPP } @@ -1977,46 +2181,97 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + where: disallowGppLogic << [ SIMPLE_GPC_DISALLOW_LOGIC, - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build(), - new UspNatV1Consent.Builder().setSaleOptOut(1).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(2).build(), - new UspNatV1Consent.Builder().setSaleOptOutNotice(0).setSaleOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSharingOptOut(1).build(), - new UspNatV1Consent.Builder().setSharingOptOutNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setSharingNotice(0).setSharingOptOut(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOut(1).build(), - new UspNatV1Consent.Builder().setTargetedAdvertisingOptOutNotice(0).setTargetedAdvertisingOptOut(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessingOptOutNotice(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataLimitUseNotice(2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2).build(), - new UspNatV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 1).build(), - new UspNatV1Consent.Builder().setPersonalDataConsents(2).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(1) + .setMspaOptOutOptionMode(2) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(2) + .setSaleOptOut(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(2) + .setSharingOptOutNotice(1) + .setSharingOptOut(1) + .setMspaServiceProviderMode(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(2) + .setSharingOptOut(1) + .setSharingNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(2) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(1) + .setTargetedAdvertisingOptOutNotice(1) + .setSaleOptOut(1) + .setSaleOptOutNotice(1) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(2) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(2) + .setMspaServiceProviderMode(2) + .setMspaOptOutOptionMode(1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 1) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(0, 2) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(1, 0) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(2) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 1, religiousBeliefs: 1, healthInfo: 1, @@ -2024,7 +2279,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { citizenshipStatus: 1, unionMembership: 1, )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataLimitUseNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -2039,7 +2294,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder() + new UsNatV1Consent.Builder() .setSensitiveDataProcessingOptOutNotice(0) .setSensitiveDataProcessing(new UsNationalSensitiveData( racialEthnicOrigin: 2, @@ -2054,14 +2309,14 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { unionMembership: 2, communicationContents: 2 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 1, biometricId: 1, idNumbers: 1, accountInfo: 1, communicationContents: 1 )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( geneticId: 2, biometricId: 2, idNumbers: 2, @@ -2071,10 +2326,75 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when privacy module contain some part of disallow logic which violates GPP validation should remove UFPD fields in request"() { + given: "Default Generic BidRequest with UFPD fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = disallowGppLogic + it.consentType = GPP + } + + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have empty UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.ext + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + where: + disallowGppLogic << [ + 'DBABLA~BAAgAAAAAAA.QA', + 'DBABLA~BCAAAAAAAAA.QA', + 'DBABLA~BAAEAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA', + 'DBABLA~BAAIAAAAAAA.QA' + ] + } + def "PBS amp call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -2106,43 +2426,46 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + where: - gppConsent | gppSid - new UspNatV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_NAT_V1 - new UspCaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CA_V1 - new UspVaV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_VA_V1 - new UspCoV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CO_V1 - new UspUtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_UT_V1 - new UspCtV1Consent.Builder().setMspaServiceProviderMode(1).build() | USP_CT_V1 + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(1).setMspaOptOutOptionMode(2).build() | US_CT_V1 } def "PBS amp call when privacy modules contain allowing settings should leave UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -2166,41 +2489,44 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids where: accountGppConfig << [ new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) ] } def "PBS amp call when regs.gpp in request is allowing should leave UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = regsGpp it.consentType = GPP } @@ -2227,38 +2553,41 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << ["", new UsNatV1Consent.Builder().build(), new UsNatV1Consent.Builder().setGpc(false).build()] } def "PBS amp call when privacy regulation have duplicate should leave UFPD fields in request and update alerts metrics"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = "" it.consentType = GPP } @@ -2274,7 +2603,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { flushMetrics(activityPbsService) and: "Account gpp privacy regulation configs with conflict" - def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: false) + def accountGppUsNatAllowConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: false) def accountGppUsNatRejectConfig = new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: []), enabled: true) def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppUsNatAllowConfig, accountGppUsNatRejectConfig]) @@ -2288,39 +2617,42 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when privacy module contain invalid property should respond with an error"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -2354,13 +2686,13 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy regulation don't match custom requirement should leave UFPD fields in request"() { given: "Store bid request with link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gpp" - def gppConsent = new UspNatV1Consent.Builder().setGpc(gpcValue).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(gpcValue).build() def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -2390,23 +2722,26 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - genericBidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 - genericBidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 - genericBidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 - genericBidderRequest.device.ifa == ampStoredRequest.device.ifa - genericBidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 - genericBidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 - genericBidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 - genericBidderRequest.user.id == ampStoredRequest.user.id - genericBidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid - genericBidderRequest.user.yob == ampStoredRequest.user.yob - genericBidderRequest.user.gender == ampStoredRequest.user.gender - genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source - genericBidderRequest.user.data == ampStoredRequest.user.data - genericBidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid - } + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo == ampStoredRequest.user.geo + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids where: gpcValue | accountLogic @@ -2419,12 +2754,12 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def "PBS amp call when privacy regulation match custom requirement should remove UFPD fields from request"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value + it.gppSid = US_NAT_V1.value it.consentString = gppConsent it.consentType = GPP } @@ -2455,45 +2790,48 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + where: - gppConsent | valueRules - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] - new UspNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] - new UspNatV1Consent.Builder().setSharingNotice(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), - new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + gppConsent | valueRules + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NOTICE_PROVIDED)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(SHARING_NOTICE, NOTICE_NOT_PROVIDED)] + new UsNatV1Consent.Builder().setPersonalDataConsents(2).build() | [new EqualityValueRule(GPC, NOTICE_PROVIDED), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, NOTICE_NOT_PROVIDED)] } def "PBS amp call when custom privacy regulation empty and normalize is disabled should respond with an error and update metric"() { given: "Store bid request with link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gpp string" - def gppConsent = new UspNatV1Consent.Builder().setGpc(true).build() + def gppConsent = new UsNatV1Consent.Builder().setGpc(true).build() def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.intValue + it.gppSid = US_NAT_V1.intValue it.consentString = gppConsent it.consentType = GPP } @@ -2509,7 +2847,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountGppConfig = new AccountGppConfig().tap { it.code = IAB_US_CUSTOM_LOGIC it.enabled = true - it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_UFPD], restrictedRule), [USP_NAT_V1], false) + it.config = GppModuleConfig.getDefaultModuleConfig(new ActivityConfig([TRANSMIT_UFPD], restrictedRule), [US_NAT_V1], false) } and: "Flush metrics" @@ -2534,13 +2872,13 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ALERT_GENERAL] == 1 + assert metrics[ALERT_GENERAL.getValue()] == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { given: "Store bid request with gpp string and link for account" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def ampStoredRequest = getBidRequestWithPersonalData(accountId) and: "amp request with link to account and gppSid" def ampRequest = AmpRequest.defaultAmpRequest.tap { @@ -2581,100 +2919,103 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.eids - !genericBidderRequest.user.data - !genericBidderRequest.user.ext + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsCaV1Consent.Builder() .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(orientation: 2)) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(0, 0) - USP_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCaV1Consent.Builder() + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2), PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspVaV1Consent.Builder() + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsVaV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsVaV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.Builder() + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCoV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCoV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UsUtV1Consent.Builder() .setSensitiveDataProcessing(new UsUtahSensitiveData(geolocation: 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) - USP_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) - - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UspCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(1, 2)) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsUtV1Consent.Builder().setKnownChildSensitiveDataConsents(0) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 0, 0) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | new UsCtV1Consent.Builder().setKnownChildSensitiveDataConsents(0, 2, 2) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), PBSUtils.getRandomNumber(0, 2), 1) - USP_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCtV1Consent.Builder() + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UsCtV1Consent.Builder() .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) } def "PBS auction call when transmit UFPD activities is rejecting requests with activityTransition false should remove only UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId) + def bidRequest = getBidRequestWithPersonalData(accountId) and: "Allow activities setup" def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) @@ -2685,39 +3026,45 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { and: "Save account config with allow activities into DB" def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities).tap { - it.config.privacy.gdpr = new AccountGdprConfig(purposes: [(Purpose.P4): new PurposeConfig(eid: new PurposeEid(activityTransition: false))]) + it.config.privacy.gdpr = new AccountGdprConfig(purposes: [(Purpose.P4): new PurposeConfig(eid: eid)]) } accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + def bidderRequest = bidder.getBidderRequest(bidRequest.id) verifyAll { - !genericBidderRequest.device.didsha1 - !genericBidderRequest.device.didmd5 - !genericBidderRequest.device.dpidsha1 - !genericBidderRequest.device.ifa - !genericBidderRequest.device.macsha1 - !genericBidderRequest.device.macmd5 - !genericBidderRequest.device.dpidmd5 - !genericBidderRequest.user.id - !genericBidderRequest.user.buyeruid - !genericBidderRequest.user.yob - !genericBidderRequest.user.gender - !genericBidderRequest.user.data + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.geo + !bidderRequest.user.ext } and: "Eids fields should have original data" - assert genericBidderRequest.user.eids == genericBidRequest.user.eids + assert bidderRequest.user.eids == bidRequest.user.eids and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 - assert metrics[DISALLOWED_COUNT_FOR_ACCOUNT.formatted(accountId)] == 1 - assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + + where: + eid << [new PurposeEid(activityTransition: false), + new PurposeEid(activityTransitionKebabCase: false)] } private static BidRequest givenBidRequestWithAccountAndUfpdData(String accountId) { @@ -2740,7 +3087,9 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { it.user.buyeruid = PBSUtils.randomString it.user.yob = PBSUtils.randomNumber it.user.gender = PBSUtils.randomString + it.user.geo = Geo.FPDGeo it.user.ext = new UserExt(data: new UserExtData(buyeruid: PBSUtils.randomString)) + it.regs.ext ?= new RegsExt() } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/LmtSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/LmtSpec.groovy index 2e6628fe276..4966fe43ddf 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/LmtSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/LmtSpec.groovy @@ -1,15 +1,24 @@ package org.prebid.server.functional.tests.privacy +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.DeviceExt -import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT +import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE -class LmtSpec extends BaseSpec { +class LmtSpec extends PrivacyBaseSpec { private static final BUGGED_IFA_VALUES = [null, "", "00000000-0000-0000-0000-000000000000"] @@ -322,6 +331,344 @@ class LmtSpec extends BaseSpec { osv << ["13.0", "12.0", "11.0"] } + def "PBS auction should mask device and user fields for auction request when device.lm = 1 was passed and with trace verbose"() { + given: "BidRequest with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + device.lmt = 1 + ext.prebid.trace = VERBOSE + } + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + } + + def "PBS auction should mask device and user fields for auction request and emit metrics when device.lm = 1 was passed and trace basic"() { + given: "BidRequest with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + device.lmt = 1 + ext.prebid.trace = BASIC + } + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == bidRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] == 1 + + and: "Account metrics shouldn't be populated" + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS auction shouldn't mask device and user fields for auction request when device.lm = 0 was passed"() { + given: "BidRequest with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + device.lmt = 0 + } + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.didsha1 == bidRequest.device.didsha1 + bidderRequest.device.didmd5 == bidRequest.device.didmd5 + bidderRequest.device.dpidsha1 == bidRequest.device.dpidsha1 + bidderRequest.device.ifa == bidRequest.device.ifa + bidderRequest.device.macsha1 == bidRequest.device.macsha1 + bidderRequest.device.macmd5 == bidRequest.device.macmd5 + bidderRequest.device.dpidmd5 == bidRequest.device.dpidmd5 + bidderRequest.device.ip == bidRequest.device.ip + bidderRequest.device.ipv6 == "af47:892b:3e98:b49a::" + bidderRequest.device.geo.lat == bidRequest.device.geo.lat + bidderRequest.device.geo.lon == bidRequest.device.geo.lon + bidderRequest.device.geo.country == bidRequest.device.geo.country + bidderRequest.device.geo.region == bidRequest.device.geo.region + bidderRequest.device.geo.utcoffset == bidRequest.device.geo.utcoffset + bidderRequest.device.geo.metro == bidRequest.device.geo.metro + bidderRequest.device.geo.city == bidRequest.device.geo.city + bidderRequest.device.geo.zip == bidRequest.device.geo.zip + bidderRequest.device.geo.accuracy == bidRequest.device.geo.accuracy + bidderRequest.device.geo.ipservice == bidRequest.device.geo.ipservice + bidderRequest.device.geo.ext == bidRequest.device.geo.ext + + bidderRequest.user.id == bidRequest.user.id + bidderRequest.user.buyeruid == bidRequest.user.buyeruid + bidderRequest.user.yob == bidRequest.user.yob + bidderRequest.user.gender == bidRequest.user.gender + bidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + bidderRequest.user.data == bidRequest.user.data + bidderRequest.user.geo.lat == bidRequest.user.geo.lat + bidderRequest.user.geo.lon == bidRequest.user.geo.lon + bidderRequest.user.ext.data.buyeruid == bidRequest.user.ext.data.buyeruid + } + + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] + } + + def "PBS amp should mask device and user fields for auction request when device.lm = 1 was passed"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Save storedRequest into DB" + def ampStoredRequest = bidRequestWithPersonalData.tap { + device.lmt = 1 + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(ampStoredRequest) + + then: "Bidder request should mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.ip == "43.77.114.0" + bidderRequest.device.ipv6 == "af47:892b:3e98:b400::" + bidderRequest.device.geo.lat == ampStoredRequest.device.geo.lat.round(2) + bidderRequest.device.geo.lon == ampStoredRequest.device.geo.lon.round(2) + + bidderRequest.device.geo.country == ampStoredRequest.device.geo.country + bidderRequest.device.geo.region == ampStoredRequest.device.geo.region + bidderRequest.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + } + + and: "Bidder request should mask device personal data" + verifyAll(bidderRequest.device) { + !didsha1 + !didmd5 + !dpidsha1 + !ifa + !macsha1 + !macmd5 + !dpidmd5 + !geo.metro + !geo.city + !geo.zip + !geo.accuracy + !geo.ipservice + !geo.ext + } + + and: "Bidder request should mask user personal data" + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_ACCOUNT_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] == 1 + } + + def "PBS amp shouldn't mask device and user fields for auction request when device.lm = 0 was passed"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Save storedRequest into DB" + def ampStoredRequest = bidRequestWithPersonalData.tap { + device.lmt = 0 + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(ampStoredRequest) + + then: "Bidder request shouldn't mask device and user personal data" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll(bidderRequest) { + bidderRequest.device.didsha1 == ampStoredRequest.device.didsha1 + bidderRequest.device.didmd5 == ampStoredRequest.device.didmd5 + bidderRequest.device.dpidsha1 == ampStoredRequest.device.dpidsha1 + bidderRequest.device.ifa == ampStoredRequest.device.ifa + bidderRequest.device.macsha1 == ampStoredRequest.device.macsha1 + bidderRequest.device.macmd5 == ampStoredRequest.device.macmd5 + bidderRequest.device.dpidmd5 == ampStoredRequest.device.dpidmd5 + bidderRequest.device.ip == ampStoredRequest.device.ip + bidderRequest.device.ipv6 == "af47:892b:3e98:b49a::" + bidderRequest.device.geo.lat == ampStoredRequest.device.geo.lat + bidderRequest.device.geo.lon == ampStoredRequest.device.geo.lon + bidderRequest.device.geo.country == ampStoredRequest.device.geo.country + bidderRequest.device.geo.region == ampStoredRequest.device.geo.region + bidderRequest.device.geo.utcoffset == ampStoredRequest.device.geo.utcoffset + bidderRequest.device.geo.metro == ampStoredRequest.device.geo.metro + bidderRequest.device.geo.city == ampStoredRequest.device.geo.city + bidderRequest.device.geo.zip == ampStoredRequest.device.geo.zip + bidderRequest.device.geo.accuracy == ampStoredRequest.device.geo.accuracy + bidderRequest.device.geo.ipservice == ampStoredRequest.device.geo.ipservice + bidderRequest.device.geo.ext == ampStoredRequest.device.geo.ext + + bidderRequest.user.id == ampStoredRequest.user.id + bidderRequest.user.buyeruid == ampStoredRequest.user.buyeruid + bidderRequest.user.yob == ampStoredRequest.user.yob + bidderRequest.user.gender == ampStoredRequest.user.gender + bidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + bidderRequest.user.data == ampStoredRequest.user.data + bidderRequest.user.geo.lat == ampStoredRequest.user.geo.lat + bidderRequest.user.geo.lon == ampStoredRequest.user.geo.lon + bidderRequest.user.ext.data.buyeruid == ampStoredRequest.user.ext.data.buyeruid + } + + and: "Metrics processed across activities shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] + assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] + } + private static getRandomAtts() { PBSUtils.getRandomElement(DeviceExt.Atts.values() as List) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index c7ca07f8124..145a33336f9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -16,21 +16,24 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.amp.ConsentType import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo -import org.prebid.server.functional.model.request.auction.RegsExt +import org.prebid.server.functional.model.request.auction.GeoExt +import org.prebid.server.functional.model.request.auction.GeoExtGeoProvider import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.model.request.auction.UserExtData import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.PbsPgConfig import org.prebid.server.functional.testcontainers.scaffolding.VendorList import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.ConsentString import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.functional.util.privacy.gpp.GppConsent -import org.prebid.server.functional.util.privacy.gpp.UspNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.UsNatV1Consent import spock.lang.Shared import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -39,10 +42,13 @@ import static org.prebid.server.functional.model.config.PurposeEnforcement.BASIC import static org.prebid.server.functional.model.config.PurposeEnforcement.FULL import static org.prebid.server.functional.model.config.PurposeEnforcement.NO import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA import static org.prebid.server.functional.model.request.amp.ConsentType.GPP import static org.prebid.server.functional.model.request.amp.ConsentType.TCF_2 import static org.prebid.server.functional.model.request.amp.ConsentType.US_PRIVACY import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.REDIRECT import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID @@ -51,23 +57,26 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionTy import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.REQUIRE_LEGITIMATE_INTEREST import static org.prebid.server.functional.util.privacy.TcfConsent.RestrictionType.UNDEFINED import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVersion.TCF_POLICY_V2 +import static org.prebid.server.functional.util.privacy.model.State.ALABAMA abstract class PrivacyBaseSpec extends BaseSpec { private static final int GEO_PRECISION = 2 - protected static final Map GENERIC_COOKIE_SYNC_CONFIG = ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : "$networkServiceContainer.rootUri/generic-usersync".toString(), - "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": false.toString()] - private static final Map OPENX_COOKIE_SYNC_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", - "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value] - private static final Map OPENX_CONFIG = ["adapters.${OPENX.value}.endpoint": "$networkServiceContainer.rootUri/auction".toString(), - "adapters.${OPENX.value}.enabled" : 'true'] + protected static final Map GENERIC_CONFIG = ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : "$networkServiceContainer.rootUri/generic-usersync".toString(), + "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": false.toString(), + "adapters.${GENERIC.value}.ortb-version" : "2.6"] + private static final Map OPENX_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", + "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value, + "adapters.${OPENX}.ortb-version" : "2.6", + "adapters.${OPENX.value}.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.${OPENX.value}.enabled" : 'true'] protected static final Map GDPR_VENDOR_LIST_CONFIG = ["gdpr.vendorlist.v2.http-endpoint-template": "$networkServiceContainer.rootUri/v2/vendor-list.json".toString(), - "gdpr.vendorlist.v3.http-endpoint-template": "$networkServiceContainer.rootUri/v3/vendor-list.json".toString()] + "gdpr.vendorlist.v3.http-endpoint-template": "$networkServiceContainer.rootUri/v3/vendor-list.json".toString()] protected static final Map SETTING_CONFIG = ["settings.enforce-valid-account": 'true'] protected static final Map GENERIC_VENDOR_CONFIG = ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String, - "gdpr.host-vendor-id" : GENERIC_VENDOR_ID as String, - "adapters.generic.ccpa-enforced" : "true"] + "gdpr.host-vendor-id" : GENERIC_VENDOR_ID as String, + "adapters.generic.ccpa-enforced" : "true"] @Shared protected static final int PURPOSES_ONLY_GVL_VERSION = PBSUtils.getRandomNumber(0, 4095) @@ -78,17 +87,31 @@ abstract class PrivacyBaseSpec extends BaseSpec { @Shared protected static final int PURPOSES_AND_LEG_INT_PURPOSES_GVL_VERSION = PBSUtils.getRandomNumberWithExclusion([PURPOSES_ONLY_GVL_VERSION, LEG_INT_PURPOSES_ONLY_GVL_VERSION, LEG_INT_AND_FLEXIBLE_PURPOSES_GVL_VERSION], 0, 4095) - private static final PbsPgConfig pgConfig = new PbsPgConfig(networkServiceContainer) + protected static final int EXPONENTIAL_BACKOFF_MAX_DELAY = 1 - protected static final Map PBS_CONFIG = OPENX_CONFIG + OPENX_COOKIE_SYNC_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + pgConfig.properties + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG + private static final Map RETRY_POLICY_EXPONENTIAL_CONFIG = [ + "gdpr.vendorlist.v2.retry-policy.exponential-backoff.delay-millis" : 1 as String, + "gdpr.vendorlist.v2.retry-policy.exponential-backoff.max-delay-millis": EXPONENTIAL_BACKOFF_MAX_DELAY as String, + "gdpr.vendorlist.v2.retry-policy.exponential-backoff.factor" : Long.MAX_VALUE as String, + "gdpr.vendorlist.v3.retry-policy.exponential-backoff.delay-millis" : 1 as String, + "gdpr.vendorlist.v3.retry-policy.exponential-backoff.max-delay-millis": EXPONENTIAL_BACKOFF_MAX_DELAY as String, + "gdpr.vendorlist.v3.retry-policy.exponential-backoff.factor" : Long.MAX_VALUE as String] + + private static final Map GDPR_EEA_COUNTRY = ["gdpr.eea-countries": "$BULGARIA.ISOAlpha2, SK, VK" as String] + + protected static final String VENDOR_LIST_PATH = "/app/prebid-server/data/vendorlist-v{VendorVersion}/{VendorVersion}.json" protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" - protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UspNatV1Consent.Builder().setGpc(true).build() + protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) + protected static final Integer MAX_INVALID_TCF_POLICY_VERSION = 63 + protected static final Integer MIN_INVALID_TCF_POLICY_VERSION = 6 @Shared protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG) + GENERIC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG + GDPR_EEA_COUNTRY) + + protected static final Map PBS_CONFIG = OPENX_CONFIG + + GENERIC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG @Shared protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(PBS_CONFIG) @@ -103,10 +126,54 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static BidRequest getBidRequestWithGeo(DistributionChannel channel = SITE) { BidRequest.getDefaultBidRequest(channel).tap { - device = new Device(ip: "43.77.114.227", ipv6: "af47:892b:3e98:b49a:a747:bda4:a6c8:aee2", - geo: new Geo(lat: PBSUtils.getRandomDecimal(0, 90), lon: PBSUtils.getRandomDecimal(0, 90))) + device = new Device( + ip: "43.77.114.227", + ipv6: "af47:892b:3e98:b49a:a747:bda4:a6c8:aee2", + geo: new Geo( + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90), + country: USA, + region: ALABAMA, + utcoffset: PBSUtils.randomNumber, + metro: PBSUtils.randomString, + city: PBSUtils.randomString, + zip: PBSUtils.randomString, + accuracy: PBSUtils.randomNumber, + ipservice: PBSUtils.randomNumber, + ext: new GeoExt(geoProvider: new GeoExtGeoProvider()), + )) user = User.defaultUser.tap { - geo = new Geo(lat: PBSUtils.getRandomDecimal(0, 90), lon: PBSUtils.getRandomDecimal(0, 90)) + geo = new Geo( + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90)) + } + } + } + + protected static BidRequest getBidRequestWithPersonalData(String accountId = null, DistributionChannel channel = SITE) { + getBidRequestWithGeo(channel).tap { + if (accountId != null) { + setAccountId(accountId) + } + ext.prebid.trace = VERBOSE + device.tap { + didsha1 = PBSUtils.randomString + didmd5 = PBSUtils.randomString + dpidsha1 = PBSUtils.randomString + ifa = PBSUtils.randomString + macsha1 = PBSUtils.randomString + macmd5 = PBSUtils.randomString + dpidmd5 = PBSUtils.randomString + } + user.tap { + customdata = PBSUtils.randomString + eids = [Eid.defaultEid] + data = [new Data(name: PBSUtils.randomString)] + buyeruid = PBSUtils.randomString + yob = PBSUtils.randomNumber + gender = PBSUtils.randomString + geo = Geo.FPDGeo + ext = new UserExt(data: new UserExtData(buyeruid: PBSUtils.randomString)) } } } @@ -119,14 +186,14 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static BidRequest getCcpaBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { getBidRequestWithGeo(channel).tap { - regs.ext = new RegsExt(usPrivacy: consentString) + regs.usPrivacy = consentString } } protected static BidRequest getGdprBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { getBidRequestWithGeo(channel).tap { - regs.ext = new RegsExt(gdpr: 1) - user = new User(ext: new UserExt(consent: consentString)) + regs.gdpr = 1 + user = new User(consent: consentString) } } @@ -160,6 +227,12 @@ abstract class PrivacyBaseSpec extends BaseSpec { def geo = bidRequest.device.geo.clone() geo.lat = PBSUtils.roundDecimal(bidRequest.device.geo.lat as BigDecimal, precision) geo.lon = PBSUtils.roundDecimal(bidRequest.device.geo.lon as BigDecimal, precision) + geo.accuracy = null + geo.zip = null + geo.metro = null + geo.city = null + geo.ext = null + geo.ipservice = null geo } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy index 64e28cf3193..882595060fa 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy @@ -27,8 +27,8 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB class TcfBasicTransmitEidsActivitiesSpec extends PrivacyBaseSpec { - private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_COOKIE_SYNC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, - "gdpr.vendorlist.v3.http-endpoint-template": null] + private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, + "gdpr.vendorlist.v3.http-endpoint-template": null] private final PrebidServerService activityPbsServiceExcludeGvl = pbsServiceFactory.getService(PBS_CONFIG) @@ -42,7 +42,7 @@ class TcfBasicTransmitEidsActivitiesSpec extends PrivacyBaseSpec { } and: "Save account config with requireConsent into DB" - def purposes = TcfUtils.getPurposeConfigsForPersonalizedAds(enforcementRequirements, true) + def purposes = TcfUtils.getPurposeConfigsForPersonalizedAdsWithSnakeCase(enforcementRequirements, true) def accountGdprConfig = new AccountGdprConfig(purposes: purposes, basicEnforcementVendors: [GENERIC.value]) def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, true)]) def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig).tap { @@ -104,7 +104,7 @@ class TcfBasicTransmitEidsActivitiesSpec extends PrivacyBaseSpec { and: "Save account config with requireConsent into DB" def purposes = TcfUtils.getPurposeConfigsForPersonalizedAds(enforcementRequirements, true) - def accountGdprConfig = new AccountGdprConfig(purposes: purposes, basicEnforcementVendors: [GENERIC.value]) + def accountGdprConfig = new AccountGdprConfig(purposes: purposes, basicEnforcementVendorsSnakeCase: [GENERIC.value]) def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, true)]) def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig).tap { config.privacy.allowActivities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy index 8ca804717ed..d0fa0583685 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy @@ -28,8 +28,8 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB class TransmitEidsOrtbConverterActivitiesSpec extends PrivacyBaseSpec { - private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_COOKIE_SYNC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, - "gdpr.vendorlist.v3.http-endpoint-template": null] + private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, + "gdpr.vendorlist.v3.http-endpoint-template": null] private final PrebidServerService activityPbsServiceExcludeGvlWithElderOrtb = pbsServiceFactory.getService(PBS_CONFIG + ["adapters.generic.ortb-version": "2.5"]) @Shared diff --git a/src/test/groovy/org/prebid/server/functional/tests/prometheus/PrometheusSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/prometheus/PrometheusSpec.groovy index 826e110299a..538705e3b68 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/prometheus/PrometheusSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/prometheus/PrometheusSpec.groovy @@ -111,7 +111,7 @@ class PrometheusSpec extends BaseSpec { and: "PBS container is prepared" def pbsContainer = new PrebidServerContainer(config) - pbsContainer.setWaitStrategy(Wait.defaultWaitStrategy()) + pbsContainer.waitingFor(Wait.defaultWaitStrategy()) when: "PBS is started" pbsContainer.start() diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy new file mode 100644 index 00000000000..3a87be7b9e7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy @@ -0,0 +1,118 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.AccountStatus +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.util.PBSUtils + +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED + +class AccountS3Spec extends StorageBaseSpec { + + protected PrebidServerService s3StorageAccountPbsService = PbsServiceFactory.getService(s3StorageConfig + + mySqlDisabledConfig + + ['settings.enforce-valid-account': 'true']) + + def "PBS should process request when active account is present in S3 storage"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Active account config" + def account = new AccountConfig(id: accountId, status: AccountStatus.ACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account) + + when: "PBS processes auction request" + def response = s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatbid" + assert response.seatbid.size() == 1 + } + + def "PBS should throw exception when inactive account is present in S3 storage"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Inactive account config" + def account = new AccountConfig(id: accountId, status: AccountStatus.INACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account) + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Account $accountId is inactive" + } + + def "PBS should throw exception when account id isn't match with bid request account id"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Account config with different accountId" + def account = new AccountConfig(id: PBSUtils.randomString, status: AccountStatus.ACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account, accountId) + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } + + def "PBS should throw exception when account is invalid in S3 storage json file"() { + given: "Default BidRequest" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Saved invalid account in AWS S3 storage" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_ACCOUNT_DIR}/${accountId}.json") + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } + + def "PBS should throw exception when account is not present in S3 storage and valid account enforced"() { + given: "Default BidRequest" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy new file mode 100644 index 00000000000..e6dda6b407c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy @@ -0,0 +1,115 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class AmpS3Spec extends StorageBaseSpec { + + def "PBS should take parameters from the stored request on S3 service when it's not specified in the request"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest) + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain parameters from the stored request" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + assert bidderRequest.site?.page == ampStoredRequest.site.page + assert bidderRequest.site?.publisher?.id == ampStoredRequest.site.publisher.id + assert !bidderRequest.imp[0]?.tagId + assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height + assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr + } + + @PendingFeature + def "PBS should throw exception when trying to take parameters from the stored request on S3 service with invalid id in file"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest).tap { + it.requestId = PBSUtils.randomNumber + } + s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest, ampRequest.tagId) + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored request found for id: ${ampRequest.tagId}" + } + + def "PBS should throw exception when trying to take parameters from request where id isn't match with stored request id"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_REQUEST_DIR}/${ampRequest.tagId}.json") + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "Can't parse Json for stored request with id ${ampRequest.tagId}" + } + + def "PBS should throw an exception when trying to take parameters from stored request on S3 service that do not exist"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored request found for id: ${ampRequest.tagId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy new file mode 100644 index 00000000000..51d39dd5af9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy @@ -0,0 +1,117 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.SecurityLevel +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class AuctionS3Spec extends StorageBaseSpec { + + def "PBS auction should populate imp[0].secure depend which value in imp stored request from S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp into S3 service" + def secureStoredRequest = PBSUtils.getRandomEnum(SecurityLevel.class) + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression.tap { + secure = secureStoredRequest + } + } + s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain imp[0].secure same value as in request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].secure == secureStoredRequest + } + + @PendingFeature + def "PBS should throw exception when trying to populate imp[0].secure from imp stored request on S3 service with impId that doesn't matches"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp with different impId into S3 service" + def secureStoredRequest = PBSUtils.getRandomNumber(0, 1) + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impId = PBSUtils.randomString + impData = Imp.defaultImpression.tap { + it.secure = secureStoredRequest + } + } + s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp, storedRequestId) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored impression found for id: ${storedRequestId}" + } + + def "PBS should throw exception when trying to populate imp[0].secure from invalid imp stored request on S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp into S3 service" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_IMPS_DIR}/${storedRequestId}.json" ) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "Can't parse Json for stored request with id ${storedRequestId}" + } + + def "PBS should throw exception when trying to populate imp[0].secure from unexciting imp stored request on S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored impression found for id: ${storedRequestId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy new file mode 100644 index 00000000000..583d6d97e06 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy @@ -0,0 +1,56 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.testcontainers.Dependencies +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.tests.BaseSpec +import org.prebid.server.functional.util.PBSUtils + +class StorageBaseSpec extends BaseSpec { + + protected static final String INVALID_FILE_BODY = 'INVALID' + protected static final String DEFAULT_BUCKET = PBSUtils.randomString.toLowerCase() + + protected static final S3Service s3Service = new S3Service(Dependencies.localStackContainer) + + def setupSpec() { + s3Service.createBucket(DEFAULT_BUCKET) + } + + def cleanupSpec() { + s3Service.purgeBucketFiles(DEFAULT_BUCKET) + s3Service.deleteBucket(DEFAULT_BUCKET) + } + + protected static Map s3StorageConfig = [ + 'settings.s3.accessKeyId' : s3Service.accessKeyId, + 'settings.s3.secretAccessKey' : s3Service.secretKeyId, + 'settings.s3.endpoint' : s3Service.endpoint, + 'settings.s3.bucket' : DEFAULT_BUCKET, + 'settings.s3.region' : s3Service.region, + 'settings.s3.force-path-style' : 'true', + 'settings.s3.accounts-dir' : S3Service.DEFAULT_ACCOUNT_DIR, + 'settings.s3.stored-imps-dir' : S3Service.DEFAULT_IMPS_DIR, + 'settings.s3.stored-requests-dir' : S3Service.DEFAULT_REQUEST_DIR, + 'settings.s3.stored-responses-dir': S3Service.DEFAULT_RESPONSE_DIR, + ] + + protected static Map mySqlDisabledConfig = + ['settings.database.type' : null, + 'settings.database.host' : null, + 'settings.database.port' : null, + 'settings.database.dbname' : null, + 'settings.database.user' : null, + 'settings.database.password' : null, + 'settings.database.pool-size' : null, + 'settings.database.provider-class' : null, + 'settings.database.account-query' : null, + 'settings.database.stored-requests-query' : null, + 'settings.database.amp-stored-requests-query': null, + 'settings.database.stored-responses-query' : null + ].asImmutable() as Map + + + protected PrebidServerService s3StoragePbsService = PbsServiceFactory.getService(s3StorageConfig + mySqlDisabledConfig) +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy new file mode 100644 index 00000000000..e07b5b71f2e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy @@ -0,0 +1,99 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class StoredResponseS3Spec extends StorageBaseSpec { + + def "PBS should return info from S3 stored auction response when it defined in request"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Stored auction response in S3 storage" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: storedAuctionResponse) + s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse) + + when: "PBS processes auction request" + def response = s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored auction response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedAuctionResponse.seat + assert response.seatbid[0]?.bid?.size() == storedAuctionResponse.bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedAuctionResponse.bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedAuctionResponse.bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedAuctionResponse.bid[0].id + + and: "PBS not send request to bidder" + assert !bidder.getRequestCount(bidRequest.id) + } + + @PendingFeature + def "PBS should throw request format exception when stored auction response id isn't match with requested response id"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Stored auction response in S3 storage with different id" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: PBSUtils.randomNumber, + storedAuctionResponse: storedAuctionResponse) + s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse, storedResponseId as String) + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " + + "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}." + } + + def "PBS should throw request format exception when invalid stored auction response defined in S3 storage"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Invalid stored auction response in S3 storage" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_RESPONSE_DIR}/${storedResponseId}.json") + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Can't parse Json for stored response with id ${storedResponseId}" + } + + def "PBS should throw request format exception when stored auction response defined in request but not defined in S3 storage"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " + + "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}." + } +} diff --git a/src/test/groovy/org/prebid/server/functional/util/AllureReporter.groovy b/src/test/groovy/org/prebid/server/functional/util/AllureReporter.groovy deleted file mode 100644 index 6ada5fe95c9..00000000000 --- a/src/test/groovy/org/prebid/server/functional/util/AllureReporter.groovy +++ /dev/null @@ -1,314 +0,0 @@ -package org.prebid.server.functional.util - -import io.qameta.allure.Allure -import io.qameta.allure.AllureLifecycle -import io.qameta.allure.Description -import io.qameta.allure.Flaky -import io.qameta.allure.Muted -import io.qameta.allure.model.Label -import io.qameta.allure.model.Link -import io.qameta.allure.model.Parameter -import io.qameta.allure.model.Status -import io.qameta.allure.model.StatusDetails -import io.qameta.allure.model.TestResult -import io.qameta.allure.util.AnnotationUtils -import org.spockframework.runtime.AbstractRunListener -import org.spockframework.runtime.extension.IGlobalExtension -import org.spockframework.runtime.extension.builtin.UnrollIterationNameProvider -import org.spockframework.runtime.model.ErrorInfo -import org.spockframework.runtime.model.FeatureInfo -import org.spockframework.runtime.model.IterationInfo -import org.spockframework.runtime.model.MethodInfo -import org.spockframework.runtime.model.SpecInfo - -import java.lang.annotation.Annotation -import java.lang.annotation.Repeatable -import java.lang.reflect.Method -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.util.stream.Collectors -import java.util.stream.Stream - -import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel -import static io.qameta.allure.util.ResultsUtils.createHostLabel -import static io.qameta.allure.util.ResultsUtils.createLanguageLabel -import static io.qameta.allure.util.ResultsUtils.createPackageLabel -import static io.qameta.allure.util.ResultsUtils.createParameter -import static io.qameta.allure.util.ResultsUtils.createParentSuiteLabel -import static io.qameta.allure.util.ResultsUtils.createSubSuiteLabel -import static io.qameta.allure.util.ResultsUtils.createSuiteLabel -import static io.qameta.allure.util.ResultsUtils.createTestClassLabel -import static io.qameta.allure.util.ResultsUtils.createTestMethodLabel -import static io.qameta.allure.util.ResultsUtils.createThreadLabel -import static io.qameta.allure.util.ResultsUtils.firstNonEmpty -import static io.qameta.allure.util.ResultsUtils.getProvidedLabels -import static io.qameta.allure.util.ResultsUtils.getStatus -import static io.qameta.allure.util.ResultsUtils.getStatusDetails -import static java.nio.charset.StandardCharsets.UTF_8 -import static java.util.Comparator.comparing -import static org.apache.commons.lang3.StringUtils.EMPTY - -/** - * This is a temporary port of https://github.com/allure-framework/allure-java/tree/master/allure-spock to add support - * for Spock 2.0. - * **/ -class AllureReporter extends AbstractRunListener implements IGlobalExtension { - - private static final String FRAMEWORK = "spock" - private static final String LANGUAGE = "groovy" - private static final String MD5 = "md5" - private static final String GIVEN = "Given:" - private static final String WHEN = "When:" - private static final String THEN = "Then:" - private static final String EXPECT = "Expect:" - private static final String WHERE = "Where:" - private static final String AND = "And:" - private static final String CLEANUP = "Cleanup:" - - private final Map stepSpockMap = new HashMap<>() - private final ThreadLocal testUuid - = InheritableThreadLocal.withInitial({ UUID.randomUUID().toString() }) - - private final AllureLifecycle lifecycle - - AllureReporter() { - this(Allure.getLifecycle()) - - this.stepSpockMap.put("SETUP", GIVEN) - this.stepSpockMap.put("WHEN", WHEN) - this.stepSpockMap.put("THEN", THEN) - this.stepSpockMap.put("EXPECT", EXPECT) - this.stepSpockMap.put("WHERE", WHERE) - this.stepSpockMap.put("AND", AND) - this.stepSpockMap.put("CLEANUP", CLEANUP) - } - - AllureReporter(AllureLifecycle lifecycle) { - this.lifecycle = lifecycle - } - - @Override - void visitSpec(SpecInfo spec) { - spec.addListener(this) - } - - @Override - void beforeIteration(IterationInfo iteration) { - String uuid = testUuid.get() - FeatureInfo feature = iteration.feature - SpecInfo spec = feature.spec - List parameters = getParameters(iteration.dataVariables) - SpecInfo subSpec = spec.subSpec - SpecInfo superSpec = spec.superSpec - String packageName = spec.package - String specName = spec.name - String testClassName = spec.reflection.name - String testMethodName = iteration.displayName - - List